| @@ -34,12 +34,24 @@ export interface InventoryResultByPage { | |||||
| total: number; | total: number; | ||||
| records: InventoryResult[]; | records: InventoryResult[]; | ||||
| } | } | ||||
| export interface UpdateInventoryLotLineStatusRequest { | |||||
| inventoryLotLineId: number; | |||||
| status: string; | |||||
| } | |||||
| export interface InventoryLotLineResultByPage { | export interface InventoryLotLineResultByPage { | ||||
| total: number; | total: number; | ||||
| records: InventoryLotLineResult[]; | records: InventoryLotLineResult[]; | ||||
| } | } | ||||
| export interface PostInventoryLotLineResponse<T = null> { | |||||
| id: number | null; | |||||
| name: string; | |||||
| code: string; | |||||
| type?: string; | |||||
| message: string | null; | |||||
| errorPosition: string | |||||
| entity?: T | T[]; | |||||
| consoCode?: string; | |||||
| } | |||||
| export const fetchLotDetail = cache(async (stockInLineId: number) => { | export const fetchLotDetail = cache(async (stockInLineId: number) => { | ||||
| return serverFetchJson<LotLineInfo>( | return serverFetchJson<LotLineInfo>( | ||||
| `${BASE_API_URL}/inventoryLotLine/lot-detail/${stockInLineId}`, | `${BASE_API_URL}/inventoryLotLine/lot-detail/${stockInLineId}`, | ||||
| @@ -49,6 +61,19 @@ export const fetchLotDetail = cache(async (stockInLineId: number) => { | |||||
| }, | }, | ||||
| ); | ); | ||||
| }); | }); | ||||
| export const updateInventoryLotLineStatus = async (data: UpdateInventoryLotLineStatusRequest) => { | |||||
| console.log("Updating inventory lot line status:", data); | |||||
| const result = await serverFetchJson<PostInventoryLotLineResponse>( | |||||
| `${BASE_API_URL}/inventoryLotLine/updateStatus`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| revalidateTag("inventory"); | |||||
| return result; | |||||
| }; | |||||
| export const fetchInventories = cache(async (data: SearchInventory) => { | export const fetchInventories = cache(async (data: SearchInventory) => { | ||||
| const queryStr = convertObjToURLSearchParams(data) | const queryStr = convertObjToURLSearchParams(data) | ||||
| @@ -12,6 +12,7 @@ import { | |||||
| PickOrderResult, | PickOrderResult, | ||||
| PreReleasePickOrderSummary, | PreReleasePickOrderSummary, | ||||
| StockOutLine, | StockOutLine, | ||||
| } from "."; | } from "."; | ||||
| import { PurchaseQcResult } from "../po/actions"; | import { PurchaseQcResult } from "../po/actions"; | ||||
| // import { BASE_API_URL } from "@/config/api"; | // import { BASE_API_URL } from "@/config/api"; | ||||
| @@ -35,6 +36,7 @@ export interface PostPickOrderResponse<T = null> { | |||||
| message: string | null; | message: string | null; | ||||
| errorPosition: string | errorPosition: string | ||||
| entity?: T | T[]; | entity?: T | T[]; | ||||
| consoCode?: string; | |||||
| } | } | ||||
| export interface PostStockOutLiineResponse<T> { | export interface PostStockOutLiineResponse<T> { | ||||
| id: number | null; | id: number | null; | ||||
| @@ -84,6 +86,7 @@ export interface PickOrderApprovalInput { | |||||
| export interface GetPickOrderInfoResponse { | export interface GetPickOrderInfoResponse { | ||||
| consoCode: string | null; | |||||
| pickOrders: GetPickOrderInfo[]; | pickOrders: GetPickOrderInfo[]; | ||||
| items: CurrentInventoryItemInfo[]; | items: CurrentInventoryItemInfo[]; | ||||
| } | } | ||||
| @@ -108,6 +111,7 @@ export interface GetPickOrderLineInfo { | |||||
| uomCode: string; | uomCode: string; | ||||
| uomDesc: string; | uomDesc: string; | ||||
| suggestedList: any[]; | suggestedList: any[]; | ||||
| pickedQty: number; | |||||
| } | } | ||||
| export interface CurrentInventoryItemInfo { | export interface CurrentInventoryItemInfo { | ||||
| @@ -137,7 +141,56 @@ export interface AssignPickOrderInputs { | |||||
| pickOrderIds: number[]; | pickOrderIds: number[]; | ||||
| assignTo: number; | assignTo: number; | ||||
| } | } | ||||
| export interface LotDetailWithStockOutLine { | |||||
| lotId: number; | |||||
| lotNo: string; | |||||
| expiryDate: string; | |||||
| location: string; | |||||
| stockUnit: string; | |||||
| availableQty: number; | |||||
| requiredQty: number; | |||||
| actualPickQty: number; | |||||
| suggestedPickLotId: number; | |||||
| lotStatus: string; | |||||
| lotAvailability: string; | |||||
| stockOutLineId?: number; | |||||
| stockOutLineStatus?: string; | |||||
| stockOutLineQty?: number; | |||||
| } | |||||
| export const resuggestPickOrder = async (pickOrderId: number) => { | |||||
| console.log("Resuggesting pick order:", pickOrderId); | |||||
| const result = await serverFetchJson<PostPickOrderResponse>( | |||||
| `${BASE_API_URL}/suggestedPickLot/resuggest/${pickOrderId}`, | |||||
| { | |||||
| method: "POST", | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| revalidateTag("pickorder"); | |||||
| return result; | |||||
| }; | |||||
| export const updateStockOutLineStatus = async (data: { | |||||
| id: number; | |||||
| status: string; | |||||
| qty?: number; | |||||
| remarks?: string; | |||||
| }) => { | |||||
| console.log("Updating stock out line status:", data); | |||||
| const result = await serverFetchJson<PostStockOutLiineResponse<StockOutLine>>( | |||||
| `${BASE_API_URL}/stockOutLine/updateStatus`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| revalidateTag("pickorder"); | |||||
| return result; | |||||
| }; | |||||
| // Missing function 1: newassignPickOrder | // Missing function 1: newassignPickOrder | ||||
| export const newassignPickOrder = async (data: AssignPickOrderInputs) => { | export const newassignPickOrder = async (data: AssignPickOrderInputs) => { | ||||
| const response = await serverFetchJson<PostPickOrderResponse>( | const response = await serverFetchJson<PostPickOrderResponse>( | ||||
| @@ -18,15 +18,12 @@ import { | |||||
| Paper, | Paper, | ||||
| Checkbox, | Checkbox, | ||||
| TablePagination, | TablePagination, | ||||
| Alert, | |||||
| AlertTitle, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | import { useCallback, useEffect, useMemo, useState } from "react"; | ||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import { | import { | ||||
| newassignPickOrder, | newassignPickOrder, | ||||
| AssignPickOrderInputs, | AssignPickOrderInputs, | ||||
| fetchPickOrderWithStockClient, | |||||
| } from "@/app/api/pickOrder/actions"; | } from "@/app/api/pickOrder/actions"; | ||||
| import { fetchNameList, NameList ,fetchNewNameList, NewNameList} from "@/app/api/user/actions"; | import { fetchNameList, NameList ,fetchNewNameList, NewNameList} from "@/app/api/user/actions"; | ||||
| import { FormProvider, useForm } from "react-hook-form"; | import { FormProvider, useForm } from "react-hook-form"; | ||||
| @@ -72,28 +69,6 @@ interface GroupedItemRow { | |||||
| items: ItemRow[]; | 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 = { | const style = { | ||||
| position: "absolute", | position: "absolute", | ||||
| top: "50%", | top: "50%", | ||||
| @@ -110,9 +85,9 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const { setIsUploading } = useUploadContext(); | const { setIsUploading } = useUploadContext(); | ||||
| // Update state to use pick order data directly | |||||
| const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<string[]>([]); // Change from number[] to string[] | |||||
| const [filteredPickOrders, setFilteredPickOrders] = useState<PickOrderRow[]>([]); | |||||
| // 修复:选择状态改为按 pick order ID 存储 | |||||
| const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<number[]>([]); | |||||
| const [filteredItems, setFilteredItems] = useState<ItemRow[]>([]); | |||||
| const [isLoadingItems, setIsLoadingItems] = useState(false); | const [isLoadingItems, setIsLoadingItems] = useState(false); | ||||
| const [pagingController, setPagingController] = useState({ | const [pagingController, setPagingController] = useState({ | ||||
| pageNum: 1, | pageNum: 1, | ||||
| @@ -122,52 +97,96 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| const [modalOpen, setModalOpen] = useState(false); | const [modalOpen, setModalOpen] = useState(false); | ||||
| const [usernameList, setUsernameList] = useState<NewNameList[]>([]); | const [usernameList, setUsernameList] = useState<NewNameList[]>([]); | ||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | ||||
| const [originalPickOrderData, setOriginalPickOrderData] = useState<PickOrderRow[]>([]); | |||||
| const [originalItemData, setOriginalItemData] = useState<ItemRow[]>([]); | |||||
| const formProps = useForm<AssignPickOrderInputs>(); | const formProps = useForm<AssignPickOrderInputs>(); | ||||
| const errors = formProps.formState.errors; | const errors = formProps.formState.errors; | ||||
| // Update the fetch function to process pick order data correctly | |||||
| // 将项目按 pick order 分组 | |||||
| const groupedItems = useMemo(() => { | |||||
| const grouped = groupBy(filteredItems, 'pickOrderId'); | |||||
| return Object.entries(grouped).map(([pickOrderId, items]) => { | |||||
| const firstItem = items[0]; | |||||
| return { | |||||
| pickOrderId: parseInt(pickOrderId), | |||||
| pickOrderCode: firstItem.pickOrderCode, | |||||
| targetDate: firstItem.targetDate, | |||||
| status: firstItem.status, | |||||
| consoCode: firstItem.consoCode, | |||||
| items: items | |||||
| } as GroupedItemRow; | |||||
| }); | |||||
| }, [filteredItems]); | |||||
| // 修复:处理 pick order 选择 | |||||
| const handlePickOrderSelect = useCallback((pickOrderId: number, checked: boolean) => { | |||||
| if (checked) { | |||||
| setSelectedPickOrderIds(prev => [...prev, pickOrderId]); | |||||
| } else { | |||||
| setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId)); | |||||
| } | |||||
| }, []); | |||||
| // 修复:检查 pick order 是否被选中 | |||||
| const isPickOrderSelected = useCallback((pickOrderId: number) => { | |||||
| return selectedPickOrderIds.includes(pickOrderId); | |||||
| }, [selectedPickOrderIds]); | |||||
| // 使用 fetchPickOrderItemsByPageClient 获取数据 | |||||
| const fetchNewPageItems = useCallback( | const fetchNewPageItems = useCallback( | ||||
| async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => { | async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => { | ||||
| console.log("=== fetchNewPageItems called ==="); | |||||
| console.log("pagingController:", pagingController); | |||||
| console.log("filterArgs:", filterArgs); | |||||
| setIsLoadingItems(true); | setIsLoadingItems(true); | ||||
| try { | try { | ||||
| const params = { | const params = { | ||||
| ...pagingController, | ...pagingController, | ||||
| ...filterArgs, | ...filterArgs, | ||||
| pageNum: (pagingController.pageNum || 1) - 1, | |||||
| pageSize: pagingController.pageSize || 10, | |||||
| // 新增:排除状态为 "assigned" 的提料单 | |||||
| //status: "pending,released,completed,cancelled" // 或者使用其他方式过滤 | |||||
| }; | }; | ||||
| console.log("Final params:", params); | |||||
| const res = await fetchPickOrderWithStockClient(params); | |||||
| const res = await fetchPickOrderItemsByPageClient(params); | |||||
| console.log("API Response:", res); | |||||
| if (res && res.records) { | if (res && res.records) { | ||||
| // Filter out assigned status if needed | |||||
| const filteredRecords = res.records.filter((pickOrder: any) => pickOrder.status !== "assigned"); | |||||
| console.log("Records received:", res.records.length); | |||||
| console.log("First record:", res.records[0]); | |||||
| // Convert pick order data to the expected format | |||||
| const pickOrderRows: PickOrderRow[] = filteredRecords.map((pickOrder: any) => ({ | |||||
| id: pickOrder.id, | |||||
| code: pickOrder.code, | |||||
| targetDate: pickOrder.targetDate, | |||||
| type: pickOrder.type, | |||||
| status: pickOrder.status, | |||||
| assignTo: pickOrder.assignTo, | |||||
| groupName: pickOrder.groupName || "No Group", | |||||
| consoCode: pickOrder.consoCode, | |||||
| pickOrderLines: pickOrder.pickOrderLines || [] | |||||
| // 新增:在前端也过滤掉 "assigned" 状态的项目 | |||||
| const filteredRecords = res.records.filter((item: any) => item.status !== "assigned"); | |||||
| const itemRows: ItemRow[] = filteredRecords.map((item: any) => ({ | |||||
| id: item.id, | |||||
| pickOrderId: item.pickOrderId, | |||||
| pickOrderCode: item.pickOrderCode, | |||||
| itemId: item.itemId, | |||||
| itemCode: item.itemCode, | |||||
| itemName: item.itemName, | |||||
| requiredQty: item.requiredQty, | |||||
| currentStock: item.currentStock ?? 0, | |||||
| unit: item.unit, | |||||
| targetDate: item.targetDate, | |||||
| status: item.status, | |||||
| consoCode: item.consoCode, | |||||
| assignTo: item.assignTo, | |||||
| groupName: item.groupName, | |||||
| })); | })); | ||||
| setOriginalPickOrderData(pickOrderRows); | |||||
| setFilteredPickOrders(pickOrderRows); | |||||
| setTotalCountItems(res.total); | |||||
| setOriginalItemData(itemRows); | |||||
| setFilteredItems(itemRows); | |||||
| setTotalCountItems(filteredRecords.length); // 使用过滤后的数量 | |||||
| } else { | } else { | ||||
| setFilteredPickOrders([]); | |||||
| console.log("No records in response"); | |||||
| setFilteredItems([]); | |||||
| setTotalCountItems(0); | setTotalCountItems(0); | ||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error fetching pick orders:", error); | |||||
| setFilteredPickOrders([]); | |||||
| console.error("Error fetching items:", error); | |||||
| setFilteredItems([]); | |||||
| setTotalCountItems(0); | setTotalCountItems(0); | ||||
| } finally { | } finally { | ||||
| setIsLoadingItems(false); | setIsLoadingItems(false); | ||||
| @@ -176,34 +195,44 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| [], | [], | ||||
| ); | ); | ||||
| // Update search criteria to match the new data structure | |||||
| const searchCriteria: Criterion<any>[] = useMemo( | const searchCriteria: Criterion<any>[] = useMemo( | ||||
| () => [ | () => [ | ||||
| { | { | ||||
| label: t("Pick Order Code"), | label: t("Pick Order Code"), | ||||
| paramName: "code", | |||||
| paramName: "pickOrderCode", | |||||
| type: "text", | type: "text", | ||||
| }, | }, | ||||
| { | |||||
| label: t("Item Code"), | |||||
| paramName: "itemCode", | |||||
| type: "text" | |||||
| }, | |||||
| { | { | ||||
| label: t("Group Name"), | |||||
| label: t("Group Code"), | |||||
| paramName: "groupName", | paramName: "groupName", | ||||
| type: "text", | type: "text", | ||||
| }, | }, | ||||
| { | |||||
| label: t("Item Name"), | |||||
| paramName: "itemName", | |||||
| type: "text", | |||||
| }, | |||||
| { | { | ||||
| label: t("Target Date From"), | label: t("Target Date From"), | ||||
| label2: t("Target Date To"), | label2: t("Target Date To"), | ||||
| paramName: "targetDate", | paramName: "targetDate", | ||||
| type: "dateRange", | type: "dateRange", | ||||
| }, | }, | ||||
| { | { | ||||
| label: t("Pick Order Status"), | label: t("Pick Order Status"), | ||||
| paramName: "status", | paramName: "status", | ||||
| type: "autocomplete", | type: "autocomplete", | ||||
| options: sortBy( | options: sortBy( | ||||
| uniqBy( | uniqBy( | ||||
| originalPickOrderData.map((pickOrder) => ({ | |||||
| value: pickOrder.status, | |||||
| label: t(upperFirst(pickOrder.status)), | |||||
| originalItemData.map((item) => ({ | |||||
| value: item.status, | |||||
| label: t(upperFirst(item.status)), | |||||
| })), | })), | ||||
| "value", | "value", | ||||
| ), | ), | ||||
| @@ -211,41 +240,45 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| ), | ), | ||||
| }, | }, | ||||
| ], | ], | ||||
| [originalPickOrderData, t], | |||||
| [originalItemData, t], | |||||
| ); | ); | ||||
| // Update search function to work with pick order data | |||||
| const handleSearch = useCallback((query: Record<string, any>) => { | const handleSearch = useCallback((query: Record<string, any>) => { | ||||
| setSearchQuery({ ...query }); | setSearchQuery({ ...query }); | ||||
| console.log("Search query:", query); | |||||
| const filtered = originalPickOrderData.filter((pickOrder) => { | |||||
| const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate); | |||||
| const filtered = originalItemData.filter((item) => { | |||||
| const itemTargetDateStr = arrayToDayjs(item.targetDate); | |||||
| const codeMatch = !query.code || | |||||
| pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase()); | |||||
| const itemCodeMatch = !query.itemCode || | |||||
| item.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); | |||||
| const groupNameMatch = !query.groupName || | |||||
| pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); | |||||
| const itemNameMatch = !query.itemName || | |||||
| item.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); | |||||
| // Date range search | |||||
| const pickOrderCodeMatch = !query.pickOrderCode || | |||||
| item.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); | |||||
| const groupNameMatch = !query.groupName || | |||||
| item.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); | |||||
| // 日期范围搜索 | |||||
| let dateMatch = true; | let dateMatch = true; | ||||
| if (query.targetDate || query.targetDateTo) { | if (query.targetDate || query.targetDateTo) { | ||||
| try { | try { | ||||
| if (query.targetDate && !query.targetDateTo) { | if (query.targetDate && !query.targetDateTo) { | ||||
| const fromDate = dayjs(query.targetDate); | const fromDate = dayjs(query.targetDate); | ||||
| dateMatch = pickOrderTargetDateStr.isSame(fromDate, 'day') || | |||||
| pickOrderTargetDateStr.isAfter(fromDate, 'day'); | |||||
| dateMatch = itemTargetDateStr.isSame(fromDate, 'day') || | |||||
| itemTargetDateStr.isAfter(fromDate, 'day'); | |||||
| } else if (!query.targetDate && query.targetDateTo) { | } else if (!query.targetDate && query.targetDateTo) { | ||||
| const toDate = dayjs(query.targetDateTo); | const toDate = dayjs(query.targetDateTo); | ||||
| dateMatch = pickOrderTargetDateStr.isSame(toDate, 'day') || | |||||
| pickOrderTargetDateStr.isBefore(toDate, 'day'); | |||||
| dateMatch = itemTargetDateStr.isSame(toDate, 'day') || | |||||
| itemTargetDateStr.isBefore(toDate, 'day'); | |||||
| } else if (query.targetDate && query.targetDateTo) { | } else if (query.targetDate && query.targetDateTo) { | ||||
| const fromDate = dayjs(query.targetDate); | const fromDate = dayjs(query.targetDate); | ||||
| const toDate = dayjs(query.targetDateTo); | const toDate = dayjs(query.targetDateTo); | ||||
| dateMatch = (pickOrderTargetDateStr.isSame(fromDate, 'day') || | |||||
| pickOrderTargetDateStr.isAfter(fromDate, 'day')) && | |||||
| (pickOrderTargetDateStr.isSame(toDate, 'day') || | |||||
| pickOrderTargetDateStr.isBefore(toDate, 'day')); | |||||
| dateMatch = (itemTargetDateStr.isSame(fromDate, 'day') || | |||||
| itemTargetDateStr.isAfter(fromDate, 'day')) && | |||||
| (itemTargetDateStr.isSame(toDate, 'day') || | |||||
| itemTargetDateStr.isBefore(toDate, 'day')); | |||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Date parsing error:", error); | console.error("Date parsing error:", error); | ||||
| @@ -255,27 +288,28 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| const statusMatch = !query.status || | const statusMatch = !query.status || | ||||
| query.status.toLowerCase() === "all" || | query.status.toLowerCase() === "all" || | ||||
| pickOrder.status?.toLowerCase().includes((query.status || "").toLowerCase()); | |||||
| item.status?.toLowerCase().includes((query.status || "").toLowerCase()); | |||||
| return codeMatch && groupNameMatch && dateMatch && statusMatch; | |||||
| return itemCodeMatch && itemNameMatch && groupNameMatch && pickOrderCodeMatch && dateMatch && statusMatch; | |||||
| }); | }); | ||||
| setFilteredPickOrders(filtered); | |||||
| }, [originalPickOrderData]); | |||||
| console.log("Filtered items count:", filtered.length); | |||||
| setFilteredItems(filtered); | |||||
| }, [originalItemData]); | |||||
| const handleReset = useCallback(() => { | const handleReset = useCallback(() => { | ||||
| setSearchQuery({}); | setSearchQuery({}); | ||||
| setFilteredPickOrders(originalPickOrderData); | |||||
| setFilteredItems(originalItemData); | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| setSearchQuery({}); | setSearchQuery({}); | ||||
| }, 0); | }, 0); | ||||
| }, [originalPickOrderData]); | |||||
| }, [originalItemData]); | |||||
| // Fix the pagination handlers | |||||
| // 修复:处理分页变化 | |||||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | const handlePageChange = useCallback((event: unknown, newPage: number) => { | ||||
| const newPagingController = { | const newPagingController = { | ||||
| ...pagingController, | ...pagingController, | ||||
| pageNum: newPage + 1, | |||||
| pageNum: newPage + 1, // API 使用 1-based 分页 | |||||
| }; | }; | ||||
| setPagingController(newPagingController); | setPagingController(newPagingController); | ||||
| }, [pagingController]); | }, [pagingController]); | ||||
| @@ -283,43 +317,27 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | ||||
| const newPageSize = parseInt(event.target.value, 10); | const newPageSize = parseInt(event.target.value, 10); | ||||
| const newPagingController = { | const newPagingController = { | ||||
| pageNum: 1, | |||||
| pageNum: 1, // 重置到第一页 | |||||
| pageSize: newPageSize, | pageSize: newPageSize, | ||||
| }; | }; | ||||
| setPagingController(newPagingController); | 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) => { | const handleAssignAndRelease = useCallback(async (data: AssignPickOrderInputs) => { | ||||
| if (selectedPickOrderIds.length === 0) return; | if (selectedPickOrderIds.length === 0) return; | ||||
| setIsUploading(true); | setIsUploading(true); | ||||
| try { | try { | ||||
| // Convert string IDs to numbers for the API | |||||
| const numericIds = selectedPickOrderIds.map(id => parseInt(id, 10)); | |||||
| // 修复:直接使用选中的 pick order IDs | |||||
| const assignRes = await newassignPickOrder({ | const assignRes = await newassignPickOrder({ | ||||
| pickOrderIds: numericIds, | |||||
| pickOrderIds: selectedPickOrderIds, | |||||
| assignTo: data.assignTo, | assignTo: data.assignTo, | ||||
| }); | }); | ||||
| if (assignRes && assignRes.code === "SUCCESS") { | if (assignRes && assignRes.code === "SUCCESS") { | ||||
| console.log("Assign successful:", assignRes); | console.log("Assign successful:", assignRes); | ||||
| setModalOpen(false); | setModalOpen(false); | ||||
| setSelectedPickOrderIds([]); // Clear selection | |||||
| setSelectedPickOrderIds([]); // 清空选择 | |||||
| fetchNewPageItems(pagingController, filterArgs); | fetchNewPageItems(pagingController, filterArgs); | ||||
| } else { | } else { | ||||
| console.error("Assign failed:", assignRes); | console.error("Assign failed:", assignRes); | ||||
| @@ -336,13 +354,15 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| formProps.reset(); | formProps.reset(); | ||||
| }, [formProps]); | }, [formProps]); | ||||
| // Component mount effect | |||||
| // 组件挂载时加载数据 | |||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log("=== Component mounted ==="); | |||||
| fetchNewPageItems(pagingController, filterArgs || {}); | fetchNewPageItems(pagingController, filterArgs || {}); | ||||
| }, []); | |||||
| }, []); // 只在组件挂载时执行一次 | |||||
| // Dependencies change effect | |||||
| // 当 pagingController 或 filterArgs 变化时重新调用 API | |||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log("=== Dependencies changed ==="); | |||||
| if (pagingController && (filterArgs || {})) { | if (pagingController && (filterArgs || {})) { | ||||
| fetchNewPageItems(pagingController, filterArgs || {}); | fetchNewPageItems(pagingController, filterArgs || {}); | ||||
| } | } | ||||
| @@ -362,8 +382,8 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| loadUsernameList(); | loadUsernameList(); | ||||
| }, []); | }, []); | ||||
| // Update the table component to work with pick order data directly | |||||
| const CustomPickOrderTable = () => { | |||||
| // 自定义分组表格组件 | |||||
| const CustomGroupedTable = () => { | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <TableContainer component={Paper}> | <TableContainer component={Paper}> | ||||
| @@ -372,7 +392,7 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell>{t("Selected")}</TableCell> | <TableCell>{t("Selected")}</TableCell> | ||||
| <TableCell>{t("Pick Order Code")}</TableCell> | <TableCell>{t("Pick Order Code")}</TableCell> | ||||
| <TableCell>{t("Group Name")}</TableCell> | |||||
| <TableCell>{t("Group Code")}</TableCell> | |||||
| <TableCell>{t("Item Code")}</TableCell> | <TableCell>{t("Item Code")}</TableCell> | ||||
| <TableCell>{t("Item Name")}</TableCell> | <TableCell>{t("Item Name")}</TableCell> | ||||
| <TableCell align="right">{t("Order Quantity")}</TableCell> | <TableCell align="right">{t("Order Quantity")}</TableCell> | ||||
| @@ -383,70 +403,72 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| {filteredPickOrders.length === 0 ? ( | |||||
| {groupedItems.length === 0 ? ( | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell colSpan={10} align="center"> | |||||
| <TableCell colSpan={9} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("No data available")} | {t("No data available")} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| ) : ( | ) : ( | ||||
| filteredPickOrders.map((pickOrder) => ( | |||||
| pickOrder.pickOrderLines.map((line: PickOrderLineRow, index: number) => ( | |||||
| <TableRow key={`${pickOrder.id}-${line.id}`}> | |||||
| {/* Checkbox - only show for first line of each pick order */} | |||||
| groupedItems.map((group) => ( | |||||
| group.items.map((item, index) => ( | |||||
| <TableRow key={item.id}> | |||||
| {/* Checkbox - 只在第一个项目显示,按 pick order 选择 */} | |||||
| <TableCell> | <TableCell> | ||||
| {index === 0 ? ( | {index === 0 ? ( | ||||
| <Checkbox | <Checkbox | ||||
| checked={isPickOrderSelected(pickOrder.id)} | |||||
| onChange={(e) => handlePickOrderSelect(pickOrder.id, e.target.checked)} | |||||
| disabled={!isEmpty(pickOrder.consoCode)} | |||||
| checked={isPickOrderSelected(group.pickOrderId)} | |||||
| onChange={(e) => handlePickOrderSelect(group.pickOrderId, e.target.checked)} | |||||
| disabled={!isEmpty(item.consoCode)} | |||||
| /> | /> | ||||
| ) : null} | ) : null} | ||||
| </TableCell> | </TableCell> | ||||
| {/* Pick Order Code - only show for first line */} | |||||
| {/* Pick Order Code - 只在第一个项目显示 */} | |||||
| <TableCell> | <TableCell> | ||||
| {index === 0 ? pickOrder.code : null} | |||||
| {index === 0 ? item.pickOrderCode : null} | |||||
| </TableCell> | </TableCell> | ||||
| {/* Group Name - only show for first line */} | |||||
| {/* Group Name */} | |||||
| <TableCell> | <TableCell> | ||||
| {index === 0 ? pickOrder.groupName : null} | |||||
| {index === 0 ? (item.groupName || "No Group") : null} | |||||
| </TableCell> | </TableCell> | ||||
| {/* Item Code */} | {/* Item Code */} | ||||
| <TableCell>{line.itemCode}</TableCell> | |||||
| <TableCell>{item.itemCode}</TableCell> | |||||
| {/* Item Name */} | {/* Item Name */} | ||||
| <TableCell>{line.itemName}</TableCell> | |||||
| <TableCell>{item.itemName}</TableCell> | |||||
| {/* Order Quantity */} | {/* Order Quantity */} | ||||
| <TableCell align="right">{line.requiredQty}</TableCell> | |||||
| <TableCell align="right">{item.requiredQty}</TableCell> | |||||
| {/* Current Stock */} | {/* Current Stock */} | ||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Typography | <Typography | ||||
| variant="body2" | variant="body2" | ||||
| color={line.availableQty && line.availableQty > 0 ? "success.main" : "error.main"} | |||||
| sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }} | |||||
| color={item.currentStock > 0 ? "success.main" : "error.main"} | |||||
| sx={{ fontWeight: item.currentStock > 0 ? 'bold' : 'normal' }} | |||||
| > | > | ||||
| {(line.availableQty || 0).toLocaleString()} | |||||
| {item.currentStock.toLocaleString()} | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Unit */} | {/* Unit */} | ||||
| <TableCell align="right">{line.uomDesc}</TableCell> | |||||
| <TableCell align="right">{item.unit}</TableCell> | |||||
| {/* Target Date - only show for first line */} | |||||
| {/* Target Date - 只在第一个项目显示 */} | |||||
| <TableCell> | <TableCell> | ||||
| {index === 0 ? ( | {index === 0 ? ( | ||||
| arrayToDayjs(pickOrder.targetDate) | |||||
| arrayToDayjs(item.targetDate) | |||||
| .add(-1, "month") | .add(-1, "month") | ||||
| .format(OUTPUT_DATE_FORMAT) | .format(OUTPUT_DATE_FORMAT) | ||||
| ) : null} | ) : null} | ||||
| </TableCell> | </TableCell> | ||||
| {/* Pick Order Status - only show for first line */} | |||||
| {/* Pick Order Status - 只在第一个项目显示 */} | |||||
| <TableCell> | <TableCell> | ||||
| {index === 0 ? upperFirst(pickOrder.status) : null} | |||||
| {index === 0 ? upperFirst(item.status) : null} | |||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| )) | )) | ||||
| @@ -456,14 +478,15 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| </Table> | </Table> | ||||
| </TableContainer> | </TableContainer> | ||||
| {/* 修复:添加分页组件 */} | |||||
| <TablePagination | <TablePagination | ||||
| component="div" | component="div" | ||||
| count={totalCountItems || 0} | count={totalCountItems || 0} | ||||
| page={(pagingController.pageNum - 1)} | |||||
| page={(pagingController.pageNum - 1)} // 转换为 0-based | |||||
| rowsPerPage={pagingController.pageSize} | rowsPerPage={pagingController.pageSize} | ||||
| onPageChange={handlePageChange} | onPageChange={handlePageChange} | ||||
| onRowsPerPageChange={handlePageSizeChange} | onRowsPerPageChange={handlePageSizeChange} | ||||
| rowsPerPageOptions={[10, 25, 50, 100]} | |||||
| rowsPerPageOptions={[10, 25, 50]} | |||||
| labelRowsPerPage={t("Rows per page")} | labelRowsPerPage={t("Rows per page")} | ||||
| labelDisplayedRows={({ from, to, count }) => | labelDisplayedRows={({ from, to, count }) => | ||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | ||||
| @@ -481,7 +504,7 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| {isLoadingItems ? ( | {isLoadingItems ? ( | ||||
| <CircularProgress size={40} /> | <CircularProgress size={40} /> | ||||
| ) : ( | ) : ( | ||||
| <CustomPickOrderTable /> | |||||
| <CustomGroupedTable /> | |||||
| )} | )} | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| @@ -556,7 +579,7 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <Typography variant="body2" color="warning.main"> | <Typography variant="body2" color="warning.main"> | ||||
| {t("This action will assign the selected pick orders.")} | |||||
| {t("This action will assign the selected pick orders to picker.")} | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| @@ -0,0 +1,209 @@ | |||||
| import React, { useCallback } from 'react'; | |||||
| import { | |||||
| Box, | |||||
| Typography, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Paper, | |||||
| Checkbox, | |||||
| TextField, | |||||
| TablePagination, | |||||
| FormControl, | |||||
| Select, | |||||
| MenuItem, | |||||
| } from '@mui/material'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| interface CreatedItem { | |||||
| itemId: number; | |||||
| itemName: string; | |||||
| itemCode: string; | |||||
| qty: number; | |||||
| uom: string; | |||||
| uomId: number; | |||||
| uomDesc: string; | |||||
| isSelected: boolean; | |||||
| currentStockBalance?: number; | |||||
| targetDate?: string | null; | |||||
| groupId?: number | null; | |||||
| } | |||||
| interface Group { | |||||
| id: number; | |||||
| name: string; | |||||
| targetDate: string; | |||||
| } | |||||
| interface CreatedItemsTableProps { | |||||
| items: CreatedItem[]; | |||||
| groups: Group[]; | |||||
| onItemSelect: (itemId: number, checked: boolean) => void; | |||||
| onQtyChange: (itemId: number, qty: number) => void; | |||||
| onGroupChange: (itemId: number, groupId: string) => void; | |||||
| pageNum: number; | |||||
| pageSize: number; | |||||
| onPageChange: (event: unknown, newPage: number) => void; | |||||
| onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | |||||
| } | |||||
| const CreatedItemsTable: React.FC<CreatedItemsTableProps> = ({ | |||||
| items, | |||||
| groups, | |||||
| onItemSelect, | |||||
| onQtyChange, | |||||
| onGroupChange, | |||||
| pageNum, | |||||
| pageSize, | |||||
| onPageChange, | |||||
| onPageSizeChange, | |||||
| }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| // Calculate pagination | |||||
| const startIndex = (pageNum - 1) * pageSize; | |||||
| const endIndex = startIndex + pageSize; | |||||
| const paginatedItems = items.slice(startIndex, endIndex); | |||||
| const handleQtyChange = useCallback((itemId: number, value: string) => { | |||||
| const numValue = Number(value); | |||||
| if (!isNaN(numValue) && numValue >= 1) { | |||||
| onQtyChange(itemId, numValue); | |||||
| } | |||||
| }, [onQtyChange]); | |||||
| return ( | |||||
| <> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell padding="checkbox" sx={{ width: '80px', minWidth: '80px' }}> | |||||
| {t("Selected")} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {t("Item")} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {t("Group")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Current Stock")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Stock Unit")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Order Quantity")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Target Date")} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {paginatedItems.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={12} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No created items")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| paginatedItems.map((item) => ( | |||||
| <TableRow key={item.itemId}> | |||||
| <TableCell padding="checkbox"> | |||||
| <Checkbox | |||||
| checked={item.isSelected} | |||||
| onChange={(e) => onItemSelect(item.itemId, e.target.checked)} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Typography variant="body2">{item.itemName}</Typography> | |||||
| <Typography variant="caption" color="textSecondary"> | |||||
| {item.itemCode} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <FormControl size="small" sx={{ minWidth: 120 }}> | |||||
| <Select | |||||
| value={item.groupId?.toString() || ""} | |||||
| onChange={(e) => onGroupChange(item.itemId, e.target.value)} | |||||
| displayEmpty | |||||
| > | |||||
| <MenuItem value=""> | |||||
| <em>{t("No Group")}</em> | |||||
| </MenuItem> | |||||
| {groups.map((group) => ( | |||||
| <MenuItem key={group.id} value={group.id.toString()}> | |||||
| {group.name} | |||||
| </MenuItem> | |||||
| ))} | |||||
| </Select> | |||||
| </FormControl> | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <Typography | |||||
| variant="body2" | |||||
| color={item.currentStockBalance && item.currentStockBalance > 0 ? "success.main" : "error.main"} | |||||
| > | |||||
| {item.currentStockBalance?.toLocaleString() || 0} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <Typography variant="body2">{item.uomDesc}</Typography> | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={item.qty || ""} | |||||
| onChange={(e) => handleQtyChange(item.itemId, e.target.value)} | |||||
| inputProps={{ | |||||
| min: 1, | |||||
| step: 1, | |||||
| style: { textAlign: 'center' } | |||||
| }} | |||||
| sx={{ | |||||
| width: '80px', | |||||
| '& .MuiInputBase-input': { | |||||
| textAlign: 'center', | |||||
| cursor: 'text' | |||||
| } | |||||
| }} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| <Typography variant="body2"> | |||||
| {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| <TablePagination | |||||
| component="div" | |||||
| count={items.length} | |||||
| page={(pageNum - 1)} | |||||
| rowsPerPage={pageSize} | |||||
| onPageChange={onPageChange} | |||||
| onRowsPerPageChange={onPageSizeChange} | |||||
| rowsPerPageOptions={[10, 25, 50]} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| labelDisplayedRows={({ from, to, count }) => | |||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||||
| } | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CreatedItemsTable; | |||||
| @@ -0,0 +1,327 @@ | |||||
| "use client"; | |||||
| import { | |||||
| Box, | |||||
| Button, | |||||
| Checkbox, | |||||
| Paper, | |||||
| Stack, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| TextField, | |||||
| Typography, | |||||
| TablePagination, | |||||
| } from "@mui/material"; | |||||
| import { useCallback, useMemo, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import QrCodeIcon from '@mui/icons-material/QrCode'; | |||||
| import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | |||||
| interface LotPickData { | |||||
| id: number; | |||||
| lotId: number; | |||||
| lotNo: string; | |||||
| expiryDate: string; | |||||
| location: string; | |||||
| stockUnit: string; | |||||
| availableQty: number; | |||||
| requiredQty: number; | |||||
| actualPickQty: number; | |||||
| lotStatus: string; | |||||
| lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; | |||||
| stockOutLineId?: number; | |||||
| stockOutLineStatus?: string; | |||||
| stockOutLineQty?: number; | |||||
| } | |||||
| interface PickQtyData { | |||||
| [lineId: number]: { | |||||
| [lotId: number]: number; | |||||
| }; | |||||
| } | |||||
| interface LotTableProps { | |||||
| lotData: LotPickData[]; | |||||
| selectedRowId: number | null; | |||||
| selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string }) | null; | |||||
| pickQtyData: PickQtyData; | |||||
| selectedLotRowId: string | null; | |||||
| selectedLotId: number | null; | |||||
| onLotSelection: (uniqueLotId: string, lotId: number) => void; | |||||
| onPickQtyChange: (lineId: number, lotId: number, value: number) => void; | |||||
| onSubmitPickQty: (lineId: number, lotId: number) => void; | |||||
| onCreateStockOutLine: (inventoryLotLineId: number) => void; | |||||
| onQcCheck: (line: GetPickOrderLineInfo, pickOrderCode: string) => void; | |||||
| onLotSelectForInput: (lot: LotPickData) => void; | |||||
| showInputBody: boolean; | |||||
| setShowInputBody: (show: boolean) => void; | |||||
| selectedLotForInput: LotPickData | null; | |||||
| generateInputBody: () => any; | |||||
| } | |||||
| const LotTable: React.FC<LotTableProps> = ({ | |||||
| lotData, | |||||
| selectedRowId, | |||||
| selectedRow, | |||||
| pickQtyData, | |||||
| selectedLotRowId, | |||||
| selectedLotId, | |||||
| onLotSelection, | |||||
| onPickQtyChange, | |||||
| onSubmitPickQty, | |||||
| onCreateStockOutLine, | |||||
| onQcCheck, | |||||
| onLotSelectForInput, | |||||
| showInputBody, | |||||
| setShowInputBody, | |||||
| selectedLotForInput, | |||||
| generateInputBody, | |||||
| }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| // 分页控制器 | |||||
| const [lotTablePagingController, setLotTablePagingController] = useState({ | |||||
| pageNum: 0, | |||||
| pageSize: 10, | |||||
| }); | |||||
| // ✅ 添加状态消息生成函数 | |||||
| const getStatusMessage = useCallback((lot: LotPickData) => { | |||||
| if (!lot.stockOutLineId) { | |||||
| return "Please finish QR code scan, QC check and pick order."; | |||||
| } | |||||
| switch (lot.stockOutLineStatus?.toUpperCase()) { | |||||
| case 'PENDING': | |||||
| return "Please finish QC check and pick order."; | |||||
| case 'COMPLETE': | |||||
| return "Please submit the pick order."; | |||||
| case 'unavailable': | |||||
| return "This order is insufficient, please pick another lot."; | |||||
| default: | |||||
| return "Please finish QR code scan, QC check and pick order."; | |||||
| } | |||||
| }, []); | |||||
| const prepareLotTableData = useMemo(() => { | |||||
| return lotData.map((lot) => ({ | |||||
| ...lot, | |||||
| id: lot.lotId, | |||||
| })); | |||||
| }, [lotData]); | |||||
| // 分页数据 | |||||
| const paginatedLotTableData = useMemo(() => { | |||||
| const startIndex = lotTablePagingController.pageNum * lotTablePagingController.pageSize; | |||||
| const endIndex = startIndex + lotTablePagingController.pageSize; | |||||
| return prepareLotTableData.slice(startIndex, endIndex); | |||||
| }, [prepareLotTableData, lotTablePagingController]); | |||||
| // 分页处理函数 | |||||
| const handleLotTablePageChange = useCallback((event: unknown, newPage: number) => { | |||||
| setLotTablePagingController(prev => ({ | |||||
| ...prev, | |||||
| pageNum: newPage, | |||||
| })); | |||||
| }, []); | |||||
| const handleLotTablePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |||||
| const newPageSize = parseInt(event.target.value, 10); | |||||
| setLotTablePagingController({ | |||||
| pageNum: 0, | |||||
| pageSize: newPageSize, | |||||
| }); | |||||
| }, []); | |||||
| return ( | |||||
| <> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>{t("Selected")}</TableCell> | |||||
| <TableCell>{t("Lot#")}</TableCell> | |||||
| <TableCell>{t("Lot Expiry Date")}</TableCell> | |||||
| <TableCell>{t("Lot Location")}</TableCell> | |||||
| <TableCell align="right">{t("Available Lot")}</TableCell> | |||||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | |||||
| <TableCell>{t("Stock Unit")}</TableCell> | |||||
| <TableCell align="center">{t("QR Code Scan")}</TableCell> | |||||
| <TableCell align="center">{t("QC Check")}</TableCell> | |||||
| <TableCell align="right">{t("Lot Actual Pick Qty")}</TableCell> | |||||
| <TableCell align="center">{t("Submit")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {paginatedLotTableData.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={11} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No data available")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| paginatedLotTableData.map((lot, index) => ( | |||||
| <TableRow key={lot.id}> | |||||
| <TableCell> | |||||
| <Checkbox | |||||
| checked={selectedLotRowId === `row_${index}`} | |||||
| onChange={() => onLotSelection(`row_${index}`, lot.lotId)} | |||||
| // ✅ Allow selection of available AND insufficient_stock lots | |||||
| disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} | |||||
| value={`row_${index}`} | |||||
| name="lot-selection" | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Box> | |||||
| <Typography>{lot.lotNo}</Typography> | |||||
| {lot.lotAvailability !== 'available' && ( | |||||
| <Typography variant="caption" color="error" display="block"> | |||||
| ({lot.lotAvailability === 'expired' ? 'Expired' : | |||||
| lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' : | |||||
| 'Unavailable'}) | |||||
| </Typography> | |||||
| )} | |||||
| </Box> | |||||
| </TableCell> | |||||
| <TableCell>{lot.expiryDate}</TableCell> | |||||
| <TableCell>{lot.location}</TableCell> | |||||
| <TableCell align="right">{lot.availableQty.toLocaleString()}</TableCell> | |||||
| <TableCell align="right">{lot.requiredQty.toLocaleString()}</TableCell> | |||||
| <TableCell>{lot.stockUnit}</TableCell> | |||||
| {/* QR Code Scan Button */} | |||||
| <TableCell align="center"> | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={() => { | |||||
| onCreateStockOutLine(lot.lotId); | |||||
| onLotSelectForInput(lot); // Show input body when button is clicked | |||||
| }} | |||||
| // ✅ Allow creation for available AND insufficient_stock lots | |||||
| disabled={(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') || Boolean(lot.stockOutLineId)} | |||||
| sx={{ | |||||
| fontSize: '0.7rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px', | |||||
| whiteSpace: 'nowrap', | |||||
| minWidth: '40px' | |||||
| }} | |||||
| startIcon={<QrCodeIcon />} // ✅ Add QR code icon | |||||
| > | |||||
| {lot.stockOutLineId ? t("Scanned") : t("Scan")} | |||||
| </Button> | |||||
| </TableCell> | |||||
| {/* QC Check Button */} | |||||
| <TableCell align="center"> | |||||
| <Button | |||||
| variant="outlined" | |||||
| size="small" | |||||
| onClick={() => { | |||||
| if (selectedRowId && selectedRow) { | |||||
| onQcCheck(selectedRow, selectedRow.pickOrderCode); | |||||
| } | |||||
| }} | |||||
| // ✅ Enable QC check only when stock out line exists | |||||
| disabled={!lot.stockOutLineId || selectedLotRowId !== `row_${index}`} | |||||
| sx={{ | |||||
| fontSize: '0.7rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px', | |||||
| whiteSpace: 'nowrap', | |||||
| minWidth: '40px' | |||||
| }} | |||||
| > | |||||
| {t("QC")} | |||||
| </Button> | |||||
| </TableCell> | |||||
| {/* Lot Actual Pick Qty */} | |||||
| <TableCell align="right"> | |||||
| <TextField | |||||
| type="number" | |||||
| value={selectedRowId ? (pickQtyData[selectedRowId]?.[lot.lotId] || 0) : 0} | |||||
| onChange={(e) => { | |||||
| if (selectedRowId) { | |||||
| onPickQtyChange( | |||||
| selectedRowId, | |||||
| lot.lotId, // This should be unique (ill.id) | |||||
| parseInt(e.target.value) || 0 | |||||
| ); | |||||
| } | |||||
| }} | |||||
| inputProps={{ min: 0, max: lot.availableQty }} | |||||
| // ✅ Allow input for available AND insufficient_stock lots | |||||
| disabled={lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable'} | |||||
| sx={{ width: '80px' }} | |||||
| /> | |||||
| </TableCell> | |||||
| {/* Submit Button */} | |||||
| <TableCell align="center"> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => { | |||||
| if (selectedRowId) { | |||||
| onSubmitPickQty(selectedRowId, lot.lotId); | |||||
| } | |||||
| }} | |||||
| // ✅ Allow submission for available AND insufficient_stock lots | |||||
| disabled={(lot.lotAvailability === 'expired' || lot.lotAvailability === 'status_unavailable') || !pickQtyData[selectedRowId!]?.[lot.lotId]} | |||||
| sx={{ | |||||
| fontSize: '0.75rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px' | |||||
| }} | |||||
| > | |||||
| {t("Submit")} | |||||
| </Button> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| {/* ✅ Status Messages Display */} | |||||
| {paginatedLotTableData.length > 0 && ( | |||||
| <Box sx={{ mt: 2, p: 2, backgroundColor: 'grey.50', borderRadius: 1 }}> | |||||
| {paginatedLotTableData.map((lot, index) => ( | |||||
| <Box key={lot.id} sx={{ mb: 1 }}> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| <strong>Lot {lot.lotNo}:</strong> {getStatusMessage(lot)} | |||||
| </Typography> | |||||
| </Box> | |||||
| ))} | |||||
| </Box> | |||||
| )} | |||||
| <TablePagination | |||||
| component="div" | |||||
| count={prepareLotTableData.length} | |||||
| page={lotTablePagingController.pageNum} | |||||
| rowsPerPage={lotTablePagingController.pageSize} | |||||
| onPageChange={handleLotTablePageChange} | |||||
| onRowsPerPageChange={handleLotTablePageSizeChange} | |||||
| rowsPerPageOptions={[10, 25, 50]} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| labelDisplayedRows={({ from, to, count }) => | |||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||||
| } | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default LotTable; | |||||
| @@ -43,6 +43,9 @@ import { | |||||
| fetchAllPickOrderDetails, | fetchAllPickOrderDetails, | ||||
| GetPickOrderInfoResponse, | GetPickOrderInfoResponse, | ||||
| GetPickOrderLineInfo, | GetPickOrderLineInfo, | ||||
| createStockOutLine, | |||||
| updateStockOutLineStatus, | |||||
| resuggestPickOrder, | |||||
| } from "@/app/api/pickOrder/actions"; | } from "@/app/api/pickOrder/actions"; | ||||
| import { EditNote } from "@mui/icons-material"; | import { EditNote } from "@mui/icons-material"; | ||||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | import { fetchNameList, NameList } from "@/app/api/user/actions"; | ||||
| @@ -64,6 +67,8 @@ import { defaultPagingController } from "../SearchResults/SearchResults"; | |||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import { dummyQCData } from "../PoDetail/dummyQcTemplate"; | import { dummyQCData } from "../PoDetail/dummyQcTemplate"; | ||||
| import { CreateStockOutLine } from "@/app/api/pickOrder/actions"; | |||||
| import LotTable from './LotTable'; | |||||
| interface Props { | interface Props { | ||||
| filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
| @@ -81,6 +86,9 @@ interface LotPickData { | |||||
| actualPickQty: number; | actualPickQty: number; | ||||
| lotStatus: string; | lotStatus: string; | ||||
| lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; | lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; | ||||
| stockOutLineId?: number; | |||||
| stockOutLineStatus?: string; | |||||
| stockOutLineQty?: number; | |||||
| } | } | ||||
| interface PickQtyData { | interface PickQtyData { | ||||
| @@ -122,6 +130,11 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| pickOrderCode: string; | pickOrderCode: string; | ||||
| qcResult?: PurchaseQcResult[]; | qcResult?: PurchaseQcResult[]; | ||||
| } | null>(null); | } | null>(null); | ||||
| const [selectedLotForQc, setSelectedLotForQc] = useState<LotPickData | null>(null); | |||||
| // ✅ Add lot selection state variables | |||||
| const [selectedLotRowId, setSelectedLotRowId] = useState<string | null>(null); | |||||
| const [selectedLotId, setSelectedLotId] = useState<number | null>(null); | |||||
| // 新增:分页控制器 | // 新增:分页控制器 | ||||
| const [mainTablePagingController, setMainTablePagingController] = useState({ | const [mainTablePagingController, setMainTablePagingController] = useState({ | ||||
| @@ -177,7 +190,34 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| useEffect(() => { | useEffect(() => { | ||||
| fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs); | fetchNewPageConsoPickOrder({ limit: 10, offset: 0 }, filterArgs); | ||||
| }, [fetchNewPageConsoPickOrder, filterArgs]); | }, [fetchNewPageConsoPickOrder, filterArgs]); | ||||
| const handleUpdateStockOutLineStatus = useCallback(async ( | |||||
| stockOutLineId: number, | |||||
| status: string, | |||||
| qty?: number | |||||
| ) => { | |||||
| try { | |||||
| const updateData = { | |||||
| id: stockOutLineId, | |||||
| status: status, | |||||
| qty: qty | |||||
| }; | |||||
| console.log("Updating stock out line status:", updateData); | |||||
| const result = await updateStockOutLineStatus(updateData); | |||||
| if (result) { | |||||
| console.log("Stock out line status updated successfully:", result); | |||||
| // Refresh lot data to show updated status | |||||
| if (selectedRowId) { | |||||
| handleRowSelect(selectedRowId); | |||||
| } | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error updating stock out line status:", error); | |||||
| } | |||||
| }, [selectedRowId]); | |||||
| const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => { | const isReleasable = useCallback((itemList: ByItemsSummary[]): boolean => { | ||||
| let isReleasable = true; | let isReleasable = true; | ||||
| for (const item of itemList) { | for (const item of itemList) { | ||||
| @@ -293,10 +333,41 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| }); | }); | ||||
| }, []); | }, []); | ||||
| const handleSubmitPickQty = useCallback((lineId: number, lotId: number) => { | |||||
| const handleSubmitPickQty = useCallback(async (lineId: number, lotId: number) => { | |||||
| const qty = pickQtyData[lineId]?.[lotId] || 0; | const qty = pickQtyData[lineId]?.[lotId] || 0; | ||||
| console.log(`提交拣货数量: Line ${lineId}, Lot ${lotId}, Qty ${qty}`); | console.log(`提交拣货数量: Line ${lineId}, Lot ${lotId}, Qty ${qty}`); | ||||
| }, [pickQtyData]); | |||||
| // ✅ Find the stock out line for this lot | |||||
| const selectedLot = lotData.find(lot => lot.lotId === lotId); | |||||
| if (!selectedLot?.stockOutLineId) { | |||||
| return; | |||||
| } | |||||
| try { | |||||
| // ✅ Update the stock out line quantity | |||||
| const updateData = { | |||||
| id: selectedLot.stockOutLineId, | |||||
| status: selectedLot.stockOutLineStatus || 'PENDING', // Keep current status | |||||
| qty: qty // Update with the submitted quantity | |||||
| }; | |||||
| console.log("Updating stock out line quantity:", updateData); | |||||
| const result = await updateStockOutLineStatus(updateData); | |||||
| if (result) { | |||||
| console.log("Stock out line quantity updated successfully:", result); | |||||
| // ✅ Refresh lot data to show updated "Qty Already Picked" | |||||
| if (selectedRowId) { | |||||
| handleRowSelect(selectedRowId); | |||||
| } | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error updating stock out line quantity:", error); | |||||
| } | |||||
| }, [pickQtyData, lotData, selectedRowId]); | |||||
| const getTotalPickedQty = useCallback((lineId: number) => { | const getTotalPickedQty = useCallback((lineId: number) => { | ||||
| const lineData = pickQtyData[lineId]; | const lineData = pickQtyData[lineId]; | ||||
| @@ -304,80 +375,65 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| return Object.values(lineData).reduce((sum, qty) => sum + qty, 0); | return Object.values(lineData).reduce((sum, qty) => sum + qty, 0); | ||||
| }, [pickQtyData]); | }, [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) => { | const handleQcCheck = useCallback(async (line: GetPickOrderLineInfo, pickOrderCode: string) => { | ||||
| console.log("QC Check clicked for:", line, pickOrderCode); | |||||
| // ✅ Get the selected lot for QC | |||||
| if (!selectedLotId) { | |||||
| return; | |||||
| } | |||||
| const selectedLot = lotData.find(lot => lot.lotId === selectedLotId); | |||||
| if (!selectedLot) { | |||||
| //alert("Selected lot not found in lot data"); | |||||
| return; | |||||
| } | |||||
| // ✅ Check if stock out line exists | |||||
| if (!selectedLot.stockOutLineId) { | |||||
| //alert("Please create a stock out line first before performing QC check"); | |||||
| return; | |||||
| } | |||||
| setSelectedLotForQc(selectedLot); | |||||
| // ✅ ALWAYS use dummy data for consistent behavior | |||||
| const transformedDummyData = dummyQCData.map(item => ({ | |||||
| id: item.id, | |||||
| code: item.code, | |||||
| name: item.name, | |||||
| itemId: line.itemId, | |||||
| lowerLimit: undefined, | |||||
| upperLimit: undefined, | |||||
| description: item.qcDescription, | |||||
| // ✅ Always reset QC result properties to undefined for fresh start | |||||
| qcPassed: undefined, | |||||
| failQty: undefined, | |||||
| remarks: undefined | |||||
| })); | |||||
| setQcItems(transformedDummyData as QcItemWithChecks[]); | |||||
| // ✅ Get existing QC results if any (for display purposes only) | |||||
| let qcResult: any[] = []; | |||||
| try { | try { | ||||
| // Try to get real data first | |||||
| const qcItemsData = await fetchQcItemCheck(line.itemId); | |||||
| console.log("QC Items from API:", qcItemsData); | |||||
| // If no data in DB, use dummy data for testing | |||||
| if (!qcItemsData || qcItemsData.length === 0) { | |||||
| console.log("No QC items in DB, using dummy data for testing"); | |||||
| // Transform dummy data to match QcItemWithChecks structure | |||||
| const transformedDummyData = dummyQCData.map(item => ({ | |||||
| id: item.id, | |||||
| code: item.code, | |||||
| name: item.name, | |||||
| itemId: line.itemId, // Use the current item's ID | |||||
| lowerLimit: undefined, | |||||
| upperLimit: undefined, | |||||
| description: item.qcDescription, | |||||
| // Add the QC result properties | |||||
| qcPassed: item.qcPassed, | |||||
| failQty: item.failQty, | |||||
| remarks: item.remarks | |||||
| })); | |||||
| setQcItems(transformedDummyData); | |||||
| } else { | |||||
| setQcItems(qcItemsData); | |||||
| } | |||||
| // 修复:处理类型不匹配问题 | |||||
| let qcResult: any[] = []; | |||||
| try { | |||||
| const rawQcResult = await fetchPickOrderQcResult(line.id); | |||||
| // 转换数据类型以匹配 PurchaseQcResult | |||||
| qcResult = rawQcResult.map((result: any) => ({ | |||||
| ...result, | |||||
| isPassed: result.isPassed || false // 添加缺失的 isPassed 属性 | |||||
| })); | |||||
| console.log("QC Result:", qcResult); | |||||
| } catch (error) { | |||||
| console.log("No existing QC result found"); | |||||
| } | |||||
| setSelectedItemForQc({ | |||||
| ...line, | |||||
| pickOrderCode, | |||||
| qcResult | |||||
| }); | |||||
| setQcModalOpen(true); | |||||
| console.log("QC Modal should open now"); | |||||
| } catch (error) { | |||||
| console.error("Error fetching QC data:", error); | |||||
| // Fallback to dummy data - transform it | |||||
| const transformedDummyData = dummyQCData.map(item => ({ | |||||
| id: item.id, | |||||
| code: item.code, | |||||
| name: item.name, | |||||
| itemId: line.itemId, | |||||
| lowerLimit: undefined, | |||||
| upperLimit: undefined, | |||||
| description: item.qcDescription, | |||||
| qcPassed: item.qcPassed, | |||||
| failQty: item.failQty, | |||||
| remarks: item.remarks | |||||
| const rawQcResult = await fetchPickOrderQcResult(line.id); | |||||
| qcResult = rawQcResult.map((result: any) => ({ | |||||
| ...result, | |||||
| isPassed: result.isPassed || false | |||||
| })); | })); | ||||
| setQcItems(transformedDummyData); | |||||
| } catch (error) { | |||||
| // No existing QC result found - this is normal | |||||
| } | } | ||||
| }, []); | |||||
| setSelectedItemForQc({ | |||||
| ...line, | |||||
| pickOrderCode, | |||||
| qcResult | |||||
| }); | |||||
| setQcModalOpen(true); | |||||
| }, [lotData, selectedLotId, setQcItems]); | |||||
| const handleCloseQcModal = useCallback(() => { | const handleCloseQcModal = useCallback(() => { | ||||
| console.log("Closing QC modal"); | console.log("Closing QC modal"); | ||||
| @@ -420,10 +476,27 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| }); | }); | ||||
| }, []); | }, []); | ||||
| // 新增:处理行选择 | |||||
| // ✅ Fix lot selection logic | |||||
| const handleLotSelection = useCallback((uniqueLotId: string, lotId: number) => { | |||||
| // If clicking the same lot, unselect it | |||||
| if (selectedLotRowId === uniqueLotId) { | |||||
| setSelectedLotRowId(null); | |||||
| setSelectedLotId(null); | |||||
| } else { | |||||
| // Select the new lot | |||||
| setSelectedLotRowId(uniqueLotId); | |||||
| setSelectedLotId(lotId); | |||||
| } | |||||
| }, [selectedLotRowId]); | |||||
| // ✅ Add function to handle row selection that resets lot selection | |||||
| const handleRowSelect = useCallback(async (lineId: number) => { | const handleRowSelect = useCallback(async (lineId: number) => { | ||||
| setSelectedRowId(lineId); | setSelectedRowId(lineId); | ||||
| // ✅ Reset lot selection when changing pick order line | |||||
| setSelectedLotRowId(null); | |||||
| setSelectedLotId(null); | |||||
| try { | try { | ||||
| const lotDetails = await fetchPickOrderLineLotDetails(lineId); | const lotDetails = await fetchPickOrderLineLotDetails(lineId); | ||||
| console.log("Lot details from API:", lotDetails); | console.log("Lot details from API:", lotDetails); | ||||
| @@ -439,7 +512,11 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| requiredQty: lot.requiredQty, | requiredQty: lot.requiredQty, | ||||
| actualPickQty: lot.actualPickQty || 0, | actualPickQty: lot.actualPickQty || 0, | ||||
| lotStatus: lot.lotStatus, | lotStatus: lot.lotStatus, | ||||
| lotAvailability: lot.lotAvailability | |||||
| lotAvailability: lot.lotAvailability, | |||||
| // ✅ Add StockOutLine fields | |||||
| stockOutLineId: lot.stockOutLineId, | |||||
| stockOutLineStatus: lot.stockOutLineStatus, | |||||
| stockOutLineQty: lot.stockOutLineQty | |||||
| })); | })); | ||||
| setLotData(realLotData); | setLotData(realLotData); | ||||
| @@ -450,25 +527,30 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| }, []); | }, []); | ||||
| const prepareMainTableData = useMemo(() => { | const prepareMainTableData = useMemo(() => { | ||||
| if (!pickOrderDetails) return []; | |||||
| return pickOrderDetails.pickOrders.flatMap((pickOrder) => | |||||
| pickOrder.pickOrderLines.map((line) => { | |||||
| // 修复:处理 availableQty 可能为 null 的情况 | |||||
| const availableQty = line.availableQty ?? 0; | |||||
| const balanceToPick = availableQty - line.requiredQty; | |||||
| return { | |||||
| ...line, | |||||
| pickOrderCode: pickOrder.code, | |||||
| targetDate: pickOrder.targetDate, | |||||
| balanceToPick: balanceToPick, | |||||
| // 确保 availableQty 不为 null | |||||
| availableQty: availableQty, | |||||
| }; | |||||
| }) | |||||
| ); | |||||
| }, [pickOrderDetails]); | |||||
| if (!pickOrderDetails) return []; | |||||
| return pickOrderDetails.pickOrders.flatMap((pickOrder) => | |||||
| pickOrder.pickOrderLines.map((line) => { | |||||
| // 修复:处理 availableQty 可能为 null 的情况 | |||||
| const availableQty = line.availableQty ?? 0; | |||||
| const balanceToPick = availableQty - line.requiredQty; | |||||
| // ✅ 使用 dayjs 进行一致的日期格式化 | |||||
| const formattedTargetDate = pickOrder.targetDate | |||||
| ? dayjs(pickOrder.targetDate).format('YYYY-MM-DD') | |||||
| : 'N/A'; | |||||
| return { | |||||
| ...line, | |||||
| pickOrderCode: pickOrder.code, | |||||
| targetDate: formattedTargetDate, // ✅ 使用 dayjs 格式化的日期 | |||||
| balanceToPick: balanceToPick, | |||||
| // 确保 availableQty 不为 null | |||||
| availableQty: availableQty, | |||||
| }; | |||||
| }) | |||||
| ); | |||||
| }, [pickOrderDetails]); | |||||
| const prepareLotTableData = useMemo(() => { | const prepareLotTableData = useMemo(() => { | ||||
| return lotData.map((lot) => ({ | return lotData.map((lot) => ({ | ||||
| @@ -502,18 +584,127 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| return null; | return null; | ||||
| }, [selectedRowId, pickOrderDetails]); | }, [selectedRowId, pickOrderDetails]); | ||||
| // Add these state variables (around line 110) | |||||
| const [selectedLotId, setSelectedLotId] = useState<string | null>(null); | |||||
| const handleInsufficientStock = useCallback(async () => { | |||||
| console.log("Insufficient stock - testing resuggest API"); | |||||
| if (!selectedRowId || !pickOrderDetails) { | |||||
| // alert("Please select a pick order line first"); | |||||
| return; | |||||
| } | |||||
| // Find the pick order ID from the selected row | |||||
| let pickOrderId: number | null = null; | |||||
| for (const pickOrder of pickOrderDetails.pickOrders) { | |||||
| const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId); | |||||
| if (foundLine) { | |||||
| pickOrderId = pickOrder.id; | |||||
| break; | |||||
| } | |||||
| } | |||||
| if (!pickOrderId) { | |||||
| // alert("Could not find pick order ID for selected line"); | |||||
| return; | |||||
| } | |||||
| try { | |||||
| console.log(`Calling resuggest API for pick order ID: ${pickOrderId}`); | |||||
| // Call the resuggest API | |||||
| const result = await resuggestPickOrder(pickOrderId); | |||||
| console.log("Resuggest API result:", result); | |||||
| if (result.code === "SUCCESS") { | |||||
| //alert(`✅ Resuggest successful!\n\nMessage: ${result.message}\n\nRemoved: ${result.message?.includes('Removed') ? 'Yes' : 'No'}\nCreated: ${result.message?.includes('created') ? 'Yes' : 'No'}`); | |||||
| // Refresh the lot data to show the new suggestions | |||||
| if (selectedRowId) { | |||||
| await handleRowSelect(selectedRowId); | |||||
| } | |||||
| // Also refresh the main pick order details | |||||
| await handleFetchAllPickOrderDetails(); | |||||
| } else { | |||||
| //alert(`❌ Resuggest failed!\n\nError: ${result.message}`); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error calling resuggest API:", error); | |||||
| //alert(`❌ Error calling resuggest API:\n\n${error instanceof Error ? error.message : 'Unknown error'}`); | |||||
| } | |||||
| }, [selectedRowId, pickOrderDetails, handleRowSelect, handleFetchAllPickOrderDetails]); | |||||
| // Add this function (around line 350) | // Add this function (around line 350) | ||||
| const handleLotSelection = useCallback((uniqueLotId: string) => { | |||||
| setSelectedLotId(uniqueLotId); | |||||
| const hasSelectedLots = useCallback((lineId: number) => { | |||||
| return selectedLotRowId !== null; | |||||
| }, [selectedLotRowId]); | |||||
| // Add state for showing input body | |||||
| const [showInputBody, setShowInputBody] = useState(false); | |||||
| const [selectedLotForInput, setSelectedLotForInput] = useState<LotPickData | null>(null); | |||||
| // Add function to handle lot selection for input body display | |||||
| const handleLotSelectForInput = useCallback((lot: LotPickData) => { | |||||
| setSelectedLotForInput(lot); | |||||
| setShowInputBody(true); | |||||
| }, []); | }, []); | ||||
| // Add this function (around line 480) | |||||
| const hasSelectedLots = useCallback((lineId: number) => { | |||||
| return selectedLotId !== null; | |||||
| }, [selectedLotId]); | |||||
| // Add function to generate input body | |||||
| const generateInputBody = useCallback((): CreateStockOutLine | null => { | |||||
| if (!selectedLotForInput || !selectedRowId || !selectedRow || !pickOrderDetails?.consoCode) { | |||||
| return null; | |||||
| } | |||||
| return { | |||||
| consoCode: pickOrderDetails.consoCode, | |||||
| pickOrderLineId: selectedRowId, | |||||
| inventoryLotLineId: selectedLotForInput.lotId, | |||||
| qty: 0.0 | |||||
| }; | |||||
| }, [selectedLotForInput, selectedRowId, selectedRow, pickOrderDetails?.consoCode]); | |||||
| // Add function to handle create stock out line | |||||
| const handleCreateStockOutLine = useCallback(async (inventoryLotLineId: number) => { | |||||
| if (!selectedRowId || !pickOrderDetails?.consoCode) { | |||||
| console.error("Missing required data for creating stock out line."); | |||||
| return; | |||||
| } | |||||
| try { | |||||
| const stockOutLineData: CreateStockOutLine = { | |||||
| consoCode: pickOrderDetails.consoCode, | |||||
| pickOrderLineId: selectedRowId, | |||||
| inventoryLotLineId: inventoryLotLineId, | |||||
| qty: 0.0 | |||||
| }; | |||||
| console.log("=== STOCK OUT LINE CREATION DEBUG ==="); | |||||
| console.log("Input Body:", JSON.stringify(stockOutLineData, null, 2)); | |||||
| // ✅ Use the correct API function | |||||
| const result = await createStockOutLine(stockOutLineData); | |||||
| console.log("Stock Out Line created:", result); | |||||
| if (result) { | |||||
| console.log("Stock out line created successfully:", result); | |||||
| //alert(`Stock out line created successfully! ID: ${result.id}`); | |||||
| // ✅ Don't refresh immediately - let user see the result first | |||||
| setShowInputBody(false); // Hide preview after successful creation | |||||
| } else { | |||||
| console.error("Failed to create stock out line: No response"); | |||||
| //alert("Failed to create stock out line: No response"); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error creating stock out line:", error); | |||||
| //alert("Error creating stock out line. Please try again."); | |||||
| } | |||||
| }, [selectedRowId, pickOrderDetails?.consoCode]); | |||||
| // 自定义主表格组件 | // 自定义主表格组件 | ||||
| const CustomMainTable = () => { | const CustomMainTable = () => { | ||||
| @@ -614,125 +805,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| ); | ); | ||||
| }; | }; | ||||
| // 自定义批次表格组件 | |||||
| const CustomLotTable = () => { | |||||
| return ( | |||||
| <> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell>{t("Selected")}</TableCell> | |||||
| <TableCell>{t("Lot#")}</TableCell> | |||||
| <TableCell>{t("Lot Expiry Date")}</TableCell> | |||||
| <TableCell>{t("Lot Location")}</TableCell> | |||||
| <TableCell align="right">{t("Available Lot")}</TableCell> | |||||
| <TableCell align="right">{t("Lot Required Pick Qty")}</TableCell> | |||||
| <TableCell align="right">{t("Lot Actual Pick Qty")}</TableCell> | |||||
| <TableCell>{t("Stock Unit")}</TableCell> | |||||
| <TableCell>{t("Submit")}</TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {paginatedLotTableData.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={9} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No data available")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| paginatedLotTableData.map((lot, index) => ( | |||||
| <TableRow key={lot.id}> | |||||
| <TableCell> | |||||
| <Checkbox | |||||
| checked={selectedLotId === `row_${index}`} | |||||
| onChange={() => handleLotSelection(`row_${index}`)} | |||||
| disabled={lot.lotAvailability !== 'available'} | |||||
| value={`row_${index}`} | |||||
| name="lot-selection" | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| <Box> | |||||
| <Typography>{lot.lotNo}</Typography> | |||||
| {lot.lotAvailability !== 'available' && ( | |||||
| <Typography variant="caption" color="error" display="block"> | |||||
| ({lot.lotAvailability === 'expired' ? 'Expired' : | |||||
| lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' : | |||||
| 'Unavailable'}) | |||||
| </Typography> | |||||
| )} | |||||
| </Box> | |||||
| </TableCell> | |||||
| <TableCell>{lot.expiryDate}</TableCell> | |||||
| <TableCell>{lot.location}</TableCell> | |||||
| <TableCell align="right">{lot.availableQty.toLocaleString()}</TableCell> | |||||
| <TableCell align="right">{lot.requiredQty.toLocaleString()}</TableCell> | |||||
| <TableCell align="right"> | |||||
| <TextField | |||||
| type="number" | |||||
| value={selectedRowId ? (pickQtyData[selectedRowId]?.[lot.lotId] || 0) : 0} | |||||
| onChange={(e) => { | |||||
| 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' }} | |||||
| /> | |||||
| </TableCell> | |||||
| <TableCell>{lot.stockUnit}</TableCell> | |||||
| <TableCell align="center"> | |||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => { | |||||
| if (selectedRowId) { | |||||
| handleSubmitPickQty(selectedRowId, lot.lotId); | |||||
| } | |||||
| }} | |||||
| disabled={lot.lotAvailability !== 'available' || !pickQtyData[selectedRowId!]?.[lot.lotId]} | |||||
| sx={{ | |||||
| fontSize: '0.75rem', | |||||
| py: 0.5, | |||||
| minHeight: '28px' | |||||
| }} | |||||
| > | |||||
| {t("Submit")} | |||||
| </Button> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| <TablePagination | |||||
| component="div" | |||||
| count={prepareLotTableData.length} | |||||
| page={lotTablePagingController.pageNum} | |||||
| rowsPerPage={lotTablePagingController.pageSize} | |||||
| onPageChange={handleLotTablePageChange} | |||||
| onRowsPerPageChange={handleLotTablePageSizeChange} | |||||
| rowsPerPageOptions={[10, 25, 50]} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| labelDisplayedRows={({ from, to, count }) => | |||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||||
| } | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| // Add search criteria | // Add search criteria | ||||
| const searchCriteria: Criterion<any>[] = useMemo( | const searchCriteria: Criterion<any>[] = useMemo( | ||||
| () => [ | () => [ | ||||
| @@ -850,7 +922,24 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| {/* 检查是否有可用的批次数据 */} | {/* 检查是否有可用的批次数据 */} | ||||
| {lotData.length > 0 ? ( | {lotData.length > 0 ? ( | ||||
| <CustomLotTable /> | |||||
| <LotTable | |||||
| lotData={lotData} | |||||
| selectedRowId={selectedRowId} | |||||
| selectedRow={selectedRow} | |||||
| pickQtyData={pickQtyData} | |||||
| selectedLotRowId={selectedLotRowId} | |||||
| selectedLotId={selectedLotId} | |||||
| onLotSelection={handleLotSelection} | |||||
| onPickQtyChange={handlePickQtyChange} | |||||
| onSubmitPickQty={handleSubmitPickQty} | |||||
| onCreateStockOutLine={handleCreateStockOutLine} | |||||
| onQcCheck={handleQcCheck} | |||||
| onLotSelectForInput={handleLotSelectForInput} | |||||
| showInputBody={showInputBody} | |||||
| setShowInputBody={setShowInputBody} | |||||
| selectedLotForInput={selectedLotForInput} | |||||
| generateInputBody={generateInputBody} | |||||
| /> | |||||
| ) : ( | ) : ( | ||||
| <Box | <Box | ||||
| sx={{ | sx={{ | ||||
| @@ -880,18 +969,6 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| {/* Action buttons below the lot table */} | {/* Action buttons below the lot table */} | ||||
| <Box sx={{ mt: 2 }}> | <Box sx={{ mt: 2 }}> | ||||
| <Stack direction="row" spacing={1}> | <Stack direction="row" spacing={1}> | ||||
| <Button | |||||
| variant="contained" | |||||
| onClick={() => { | |||||
| if (selectedRowId && selectedRow) { | |||||
| handleQcCheck(selectedRow, selectedRow.pickOrderCode); | |||||
| } | |||||
| }} | |||||
| disabled={!hasSelectedLots(selectedRowId!)} | |||||
| sx={{ whiteSpace: 'nowrap' }} | |||||
| > | |||||
| {t("Qc Check")} {selectedLotId ? '(1 selected)' : '(none selected)'} | |||||
| </Button> | |||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| onClick={() => handleInsufficientStock()} | onClick={() => handleInsufficientStock()} | ||||
| @@ -938,6 +1015,13 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => { | |||||
| warehouse={[]} | warehouse={[]} | ||||
| qcItems={qcItems} | qcItems={qcItems} | ||||
| setQcItems={setQcItems} | setQcItems={setQcItems} | ||||
| selectedLotId={selectedLotForQc?.stockOutLineId} | |||||
| onStockOutLineUpdate={() => { | |||||
| if (selectedRowId) { | |||||
| handleRowSelect(selectedRowId); | |||||
| } | |||||
| }} | |||||
| lotData={lotData} | |||||
| /> | /> | ||||
| )} | )} | ||||
| </Stack> | </Stack> | ||||
| @@ -1,6 +1,6 @@ | |||||
| "use client"; | "use client"; | ||||
| import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; | |||||
| import { GetPickOrderLineInfo, updateStockOutLineStatus } from "@/app/api/pickOrder/actions"; | |||||
| import { QcItemWithChecks } from "@/app/api/qc"; | import { QcItemWithChecks } from "@/app/api/qc"; | ||||
| import { PurchaseQcResult } from "@/app/api/po/actions"; | import { PurchaseQcResult } from "@/app/api/po/actions"; | ||||
| import { | import { | ||||
| @@ -31,6 +31,9 @@ import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | |||||
| import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable"; | import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable"; | ||||
| import EscalationComponent from "../PoDetail/EscalationComponent"; | import EscalationComponent from "../PoDetail/EscalationComponent"; | ||||
| import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions"; | import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions"; | ||||
| import { | |||||
| updateInventoryLotLineStatus | |||||
| } from "@/app/api/inventory/actions"; // ✅ 导入新的 API | |||||
| // Define QcData interface locally | // Define QcData interface locally | ||||
| interface ExtendedQcItem extends QcItemWithChecks { | interface ExtendedQcItem extends QcItemWithChecks { | ||||
| @@ -79,8 +82,27 @@ interface Props extends CommonProps { | |||||
| }; | }; | ||||
| qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem | qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem | ||||
| setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>; // Change to ExtendedQcItem | setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>; // Change to ExtendedQcItem | ||||
| // ✅ Add props for stock out line update | |||||
| selectedLotId?: number; | |||||
| onStockOutLineUpdate?: () => void; | |||||
| lotData: LotPickData[]; | |||||
| } | |||||
| interface LotPickData { | |||||
| id: number; | |||||
| lotId: number; | |||||
| lotNo: string; | |||||
| expiryDate: string; | |||||
| location: string; | |||||
| stockUnit: string; | |||||
| availableQty: number; | |||||
| requiredQty: number; | |||||
| actualPickQty: number; | |||||
| lotStatus: string; | |||||
| lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable'; | |||||
| stockOutLineId?: number; | |||||
| stockOutLineStatus?: string; | |||||
| stockOutLineQty?: number; | |||||
| } | } | ||||
| const PickQcStockInModalVer3: React.FC<Props> = ({ | const PickQcStockInModalVer3: React.FC<Props> = ({ | ||||
| open, | open, | ||||
| onClose, | onClose, | ||||
| @@ -90,6 +112,9 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| warehouse, | warehouse, | ||||
| qcItems, | qcItems, | ||||
| setQcItems, | setQcItems, | ||||
| selectedLotId, | |||||
| onStockOutLineUpdate, | |||||
| lotData, | |||||
| }) => { | }) => { | ||||
| const { | const { | ||||
| t, | t, | ||||
| @@ -108,7 +133,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| const formProps = useForm<any>({ | const formProps = useForm<any>({ | ||||
| defaultValues: { | defaultValues: { | ||||
| qcAccept: true, | qcAccept: true, | ||||
| acceptQty: itemDetail.requiredQty ?? 0, | |||||
| acceptQty: null, | |||||
| qcDecision: "1", // Default to accept | qcDecision: "1", // Default to accept | ||||
| ...itemDetail, | ...itemDetail, | ||||
| }, | }, | ||||
| @@ -168,78 +193,145 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| } | } | ||||
| }; | }; | ||||
| // Submit with QcComponent-style decision handling | |||||
| // ✅ 修改:在组件开始时自动设置失败数量 | |||||
| useEffect(() => { | |||||
| if (itemDetail && qcItems.length > 0) { | |||||
| // ✅ 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty | |||||
| const updatedQcItems = qcItems.map(item => ({ | |||||
| ...item, | |||||
| failQty: itemDetail.requiredQty || 0 // 使用 Lot Required Pick Qty | |||||
| })); | |||||
| setQcItems(updatedQcItems); | |||||
| } | |||||
| }, [itemDetail, qcItems.length]); | |||||
| // ✅ 修改:移除 alert 弹窗,改为控制台日志 | |||||
| const onSubmitQc = useCallback<SubmitHandler<any>>( | const onSubmitQc = useCallback<SubmitHandler<any>>( | ||||
| async (data, event) => { | async (data, event) => { | ||||
| setIsSubmitting(true); | setIsSubmitting(true); | ||||
| try { | try { | ||||
| const qcAccept = qcDecision === "1"; | const qcAccept = qcDecision === "1"; | ||||
| const acceptQty = Number(accQty) || itemDetail.requiredQty; | |||||
| const acceptQty = Number(accQty) || null; | |||||
| const validationErrors : string[] = []; | const validationErrors : string[] = []; | ||||
| const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined); | const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined); | ||||
| if (itemsWithoutResult.length > 0) { | if (itemsWithoutResult.length > 0) { | ||||
| validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`); | validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`); | ||||
| } | } | ||||
| 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(", ")}`); | |||||
| } | |||||
| if (qcDecision === "1" && (acceptQty === undefined || acceptQty <= 0)) { | |||||
| validationErrors.push("Accept quantity must be greater than 0"); | |||||
| } | |||||
| if (validationErrors.length > 0) { | if (validationErrors.length > 0) { | ||||
| alert(`QC failed: ${validationErrors.join(", ")}`); | |||||
| console.error(`QC validation failed: ${validationErrors.join(", ")}`); | |||||
| return; | return; | ||||
| } | } | ||||
| const qcData = { | const qcData = { | ||||
| qcAccept, | qcAccept, | ||||
| acceptQty, | acceptQty, | ||||
| qcItems: qcItems.map(item => ({ | qcItems: qcItems.map(item => ({ | ||||
| id: item.id, | id: item.id, | ||||
| qcItem: item.code, // Use code instead of qcItem | |||||
| qcDescription: item.description || "", // Use description instead of qcDescription | |||||
| qcItem: item.code, | |||||
| qcDescription: item.description || "", | |||||
| isPassed: item.qcPassed, | isPassed: item.qcPassed, | ||||
| failQty: item.qcPassed ? 0 : (item.failQty ?? 0), | |||||
| failQty: item.qcPassed ? 0 : (itemDetail?.requiredQty || 0), | |||||
| remarks: item.remarks || "", | remarks: item.remarks || "", | ||||
| })), | })), | ||||
| }; | }; | ||||
| console.log("Submitting QC data:", qcData); | console.log("Submitting QC data:", qcData); | ||||
| const saveSuccess = await saveQcResults(qcData); | const saveSuccess = await saveQcResults(qcData); | ||||
| if (!saveSuccess) { | if (!saveSuccess) { | ||||
| alert("Failed to save QC results"); | |||||
| console.error("Failed to save QC results"); | |||||
| return; | return; | ||||
| } | } | ||||
| // Show success message | |||||
| alert("QC results saved successfully!"); | |||||
| // ✅ Fix: Update stock out line status based on QC decision | |||||
| if (selectedLotId && qcData.qcAccept) { | |||||
| try { | |||||
| const allPassed = qcData.qcItems.every(item => item.isPassed); | |||||
| // ✅ Fix: Use correct backend enum values | |||||
| const newStockOutLineStatus = allPassed ? 'completed' : 'rejected'; | |||||
| console.log("Updating stock out line status after QC:", { | |||||
| stockOutLineId: selectedLotId, | |||||
| newStatus: newStockOutLineStatus | |||||
| }); | |||||
| // ✅ Fix: 1. Update stock out line status with required qty field | |||||
| await updateStockOutLineStatus({ | |||||
| id: selectedLotId, | |||||
| status: newStockOutLineStatus, | |||||
| qty: itemDetail?.requiredQty || 0 // ✅ Add required qty field | |||||
| }); | |||||
| // ✅ Fix: 2. If QC failed, also update inventory lot line status | |||||
| if (!allPassed) { | |||||
| try { | |||||
| // ✅ Fix: Get the correct lot data | |||||
| const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId); | |||||
| if (selectedLot) { | |||||
| console.log("Updating inventory lot line status for failed QC:", { | |||||
| inventoryLotLineId: selectedLot.lotId, | |||||
| status: 'unavailable' | |||||
| }); | |||||
| await updateInventoryLotLineStatus({ | |||||
| inventoryLotLineId: selectedLot.lotId, | |||||
| status: 'unavailable' // ✅ Use correct backend enum value | |||||
| }); | |||||
| console.log("Inventory lot line status updated to unavailable"); | |||||
| } else { | |||||
| console.warn("Selected lot not found for inventory lot line status update"); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Failed to update inventory lot line status:", error); | |||||
| // ✅ Don't fail the entire operation, just log the error | |||||
| } | |||||
| } | |||||
| console.log("Stock out line status updated successfully after QC"); | |||||
| // ✅ Call callback to refresh data | |||||
| if (onStockOutLineUpdate) { | |||||
| onStockOutLineUpdate(); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error updating stock out line status after QC:", error); | |||||
| // ✅ Log detailed error information | |||||
| if (error instanceof Error) { | |||||
| console.error("Error details:", error.message); | |||||
| console.error("Error stack:", error.stack); | |||||
| } | |||||
| // ✅ Don't fail the entire QC submission, just log the error | |||||
| } | |||||
| } | |||||
| console.log("QC results saved successfully!"); | |||||
| // ✅ Show warning dialog for failed QC items | |||||
| if (!qcData.qcItems.every((q) => q.isPassed) && qcData.qcAccept) { | if (!qcData.qcItems.every((q) => q.isPassed) && qcData.qcAccept) { | ||||
| submitDialogWithWarning(() => { | submitDialogWithWarning(() => { | ||||
| closeHandler?.({}, 'escapeKeyDown'); | closeHandler?.({}, 'escapeKeyDown'); | ||||
| }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""}); | }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""}); | ||||
| return; | return; | ||||
| } | } | ||||
| closeHandler?.({}, 'escapeKeyDown'); | closeHandler?.({}, 'escapeKeyDown'); | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error in QC submission:", error); | console.error("Error in QC submission:", error); | ||||
| alert("Error saving QC results: " + (error as Error).message); | |||||
| // ✅ Enhanced error logging | |||||
| if (error instanceof Error) { | |||||
| console.error("Error details:", error.message); | |||||
| console.error("Error stack:", error.stack); | |||||
| } | |||||
| } finally { | } finally { | ||||
| setIsSubmitting(false); | setIsSubmitting(false); | ||||
| } | } | ||||
| }, | }, | ||||
| [qcItems, closeHandler, t, itemDetail, qcDecision, accQty], | |||||
| [qcItems, closeHandler, t, itemDetail, qcDecision, accQty, selectedLotId, onStockOutLineUpdate, lotData], | |||||
| ); | ); | ||||
| // DataGrid columns (QcComponent style) | // DataGrid columns (QcComponent style) | ||||
| @@ -307,20 +399,22 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| <TextField | <TextField | ||||
| type="number" | type="number" | ||||
| size="small" | size="small" | ||||
| value={!params.row.qcPassed ? (params.value ?? "") : "0"} | |||||
| // ✅ 修改:失败项目自动显示 Lot Required Pick Qty | |||||
| value={!params.row.qcPassed ? (itemDetail?.requiredQty || 0) : 0} | |||||
| disabled={params.row.qcPassed} | disabled={params.row.qcPassed} | ||||
| onChange={(e) => { | |||||
| 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)) | |||||
| ); | |||||
| }} | |||||
| // ✅ 移除 onChange,因为数量是固定的 | |||||
| // onChange={(e) => { | |||||
| // 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()} | onClick={(e) => e.stopPropagation()} | ||||
| onMouseDown={(e) => e.stopPropagation()} | onMouseDown={(e) => e.stopPropagation()} | ||||
| onKeyDown={(e) => e.stopPropagation()} | onKeyDown={(e) => e.stopPropagation()} | ||||
| inputProps={{ min: 0 }} | |||||
| inputProps={{ min: 0, max: itemDetail?.requiredQty || 0 }} | |||||
| sx={{ width: "100%" }} | sx={{ width: "100%" }} | ||||
| /> | /> | ||||
| ), | ), | ||||
| @@ -374,6 +468,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}> | <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}> | ||||
| Group A - 急凍貨類 (QCA1-MEAT01) | Group A - 急凍貨類 (QCA1-MEAT01) | ||||
| </Typography> | </Typography> | ||||
| <Typography variant="subtitle1" sx={{ color: '#666' }}> | <Typography variant="subtitle1" sx={{ color: '#666' }}> | ||||
| <b>品檢類型</b>:OQC | <b>品檢類型</b>:OQC | ||||
| </Typography> | </Typography> | ||||
| @@ -381,6 +476,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| 記錄探測溫度的時間,請在1小時内完成出庫盤點,以保障食品安全<br/> | 記錄探測溫度的時間,請在1小時内完成出庫盤點,以保障食品安全<br/> | ||||
| 監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標 | 監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標 | ||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| <StyledDataGrid | <StyledDataGrid | ||||
| @@ -434,7 +530,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| value={(qcDecision == 1)? accQty : 0 } | value={(qcDecision == 1)? accQty : 0 } | ||||
| disabled={qcDecision != 1} | disabled={qcDecision != 1} | ||||
| {...register("acceptQty", { | {...register("acceptQty", { | ||||
| required: "acceptQty required!", | |||||
| //required: "acceptQty required!", | |||||
| })} | })} | ||||
| error={Boolean(errors.acceptQty)} | error={Boolean(errors.acceptQty)} | ||||
| helperText={errors.acceptQty?.message?.toString() || ""} | helperText={errors.acceptQty?.message?.toString() || ""} | ||||
| @@ -466,6 +562,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({ | |||||
| forSupervisor={false} | forSupervisor={false} | ||||
| isCollapsed={isCollapsed} | isCollapsed={isCollapsed} | ||||
| setIsCollapsed={setIsCollapsed} | setIsCollapsed={setIsCollapsed} | ||||
| //escalationCombo={[]} // ✅ Add missing prop | |||||
| /> | /> | ||||
| </Grid> | </Grid> | ||||
| )} | )} | ||||
| @@ -0,0 +1,242 @@ | |||||
| import React, { useCallback } from 'react'; | |||||
| import { | |||||
| Box, | |||||
| Typography, | |||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Paper, | |||||
| Checkbox, | |||||
| TextField, | |||||
| TablePagination, | |||||
| FormControl, | |||||
| Select, | |||||
| MenuItem, | |||||
| } from '@mui/material'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| interface SearchItemWithQty { | |||||
| id: number; | |||||
| label: string; | |||||
| qty: number | null; | |||||
| currentStockBalance?: number; | |||||
| uomDesc?: string; | |||||
| targetDate?: string | null; | |||||
| groupId?: number | null; | |||||
| } | |||||
| interface Group { | |||||
| id: number; | |||||
| name: string; | |||||
| targetDate: string; | |||||
| } | |||||
| interface SearchResultsTableProps { | |||||
| items: SearchItemWithQty[]; | |||||
| selectedItemIds: (string | number)[]; | |||||
| groups: Group[]; | |||||
| onItemSelect: (itemId: number, checked: boolean) => void; | |||||
| onQtyChange: (itemId: number, qty: number | null) => void; | |||||
| onQtyBlur: (itemId: number) => void; | |||||
| onGroupChange: (itemId: number, groupId: string) => void; | |||||
| isItemInCreated: (itemId: number) => boolean; | |||||
| pageNum: number; | |||||
| pageSize: number; | |||||
| onPageChange: (event: unknown, newPage: number) => void; | |||||
| onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void; | |||||
| } | |||||
| const SearchResultsTable: React.FC<SearchResultsTableProps> = ({ | |||||
| items, | |||||
| selectedItemIds, | |||||
| groups, | |||||
| onItemSelect, | |||||
| onQtyChange, | |||||
| onGroupChange, | |||||
| onQtyBlur, | |||||
| isItemInCreated, | |||||
| pageNum, | |||||
| pageSize, | |||||
| onPageChange, | |||||
| onPageSizeChange, | |||||
| }) => { | |||||
| const { t } = useTranslation("pickOrder"); | |||||
| // Calculate pagination | |||||
| const startIndex = (pageNum - 1) * pageSize; | |||||
| const endIndex = startIndex + pageSize; | |||||
| const paginatedResults = items.slice(startIndex, endIndex); | |||||
| const handleQtyChange = useCallback((itemId: number, value: string) => { | |||||
| // Only allow numbers | |||||
| if (value === "" || /^\d+$/.test(value)) { | |||||
| const numValue = value === "" ? null : Number(value); | |||||
| onQtyChange(itemId, numValue); | |||||
| } | |||||
| }, [onQtyChange]); | |||||
| return ( | |||||
| <> | |||||
| <TableContainer component={Paper}> | |||||
| <Table> | |||||
| <TableHead> | |||||
| <TableRow> | |||||
| <TableCell padding="checkbox" sx={{ width: '80px', minWidth: '80px' }}> | |||||
| {t("Selected")} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {t("Item")} | |||||
| </TableCell> | |||||
| <TableCell> | |||||
| {t("Group")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Current Stock")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Stock Unit")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Order Quantity")} | |||||
| </TableCell> | |||||
| <TableCell align="right"> | |||||
| {t("Target Date")} | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| </TableHead> | |||||
| <TableBody> | |||||
| {paginatedResults.length === 0 ? ( | |||||
| <TableRow> | |||||
| <TableCell colSpan={12} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | |||||
| {t("No data available")} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| ) : ( | |||||
| paginatedResults.map((item) => ( | |||||
| <TableRow key={item.id}> | |||||
| <TableCell padding="checkbox"> | |||||
| <Checkbox | |||||
| checked={selectedItemIds.includes(item.id)} | |||||
| onChange={(e) => onItemSelect(item.id, e.target.checked)} | |||||
| disabled={isItemInCreated(item.id)} | |||||
| /> | |||||
| </TableCell> | |||||
| {/* Item */} | |||||
| <TableCell> | |||||
| <Box> | |||||
| <Typography variant="body2"> | |||||
| {item.label.split(' - ')[1] || item.label} | |||||
| </Typography> | |||||
| <Typography variant="caption" color="textSecondary"> | |||||
| {item.label.split(' - ')[0] || ''} | |||||
| </Typography> | |||||
| </Box> | |||||
| </TableCell> | |||||
| {/* Group */} | |||||
| <TableCell> | |||||
| <FormControl size="small" sx={{ minWidth: 120 }}> | |||||
| <Select | |||||
| value={item.groupId?.toString() || ""} | |||||
| onChange={(e) => onGroupChange(item.id, e.target.value)} | |||||
| displayEmpty | |||||
| disabled={isItemInCreated(item.id)} | |||||
| > | |||||
| <MenuItem value=""> | |||||
| <em>{t("No Group")}</em> | |||||
| </MenuItem> | |||||
| {groups.map((group) => ( | |||||
| <MenuItem key={group.id} value={group.id.toString()}> | |||||
| {group.name} | |||||
| </MenuItem> | |||||
| ))} | |||||
| </Select> | |||||
| </FormControl> | |||||
| </TableCell> | |||||
| {/* Current Stock */} | |||||
| <TableCell align="right"> | |||||
| <Typography | |||||
| variant="body2" | |||||
| color={item.currentStockBalance && item.currentStockBalance > 0 ? "success.main" : "error.main"} | |||||
| sx={{ fontWeight: item.currentStockBalance && item.currentStockBalance > 0 ? 'bold' : 'normal' }} | |||||
| > | |||||
| {item.currentStockBalance || 0} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| {/* Stock Unit */} | |||||
| <TableCell align="right"> | |||||
| <Typography variant="body2"> | |||||
| {item.uomDesc || "-"} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| {/* Order Quantity */} | |||||
| <TextField | |||||
| type="number" | |||||
| size="small" | |||||
| value={item.qty || ""} | |||||
| onChange={(e) => { | |||||
| const value = e.target.value; | |||||
| // Only allow numbers | |||||
| if (value === "" || /^\d+$/.test(value)) { | |||||
| const numValue = value === "" ? null : Number(value); | |||||
| onQtyChange(item.id, numValue); | |||||
| } | |||||
| }} | |||||
| onBlur={() => { | |||||
| // Trigger auto-add check when user finishes input (clicks elsewhere) | |||||
| onQtyBlur(item.id); // ← Change this to call onQtyBlur instead! | |||||
| }} | |||||
| inputProps={{ | |||||
| style: { textAlign: 'center' } | |||||
| }} | |||||
| sx={{ | |||||
| width: '80px', | |||||
| '& .MuiInputBase-input': { | |||||
| textAlign: 'center', | |||||
| cursor: 'text' | |||||
| } | |||||
| }} | |||||
| disabled={isItemInCreated(item.id)} | |||||
| /> | |||||
| {/* Target Date */} | |||||
| <TableCell align="right"> | |||||
| <Typography variant="body2"> | |||||
| {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} | |||||
| </Typography> | |||||
| </TableCell> | |||||
| </TableRow> | |||||
| )) | |||||
| )} | |||||
| </TableBody> | |||||
| </Table> | |||||
| </TableContainer> | |||||
| <TablePagination | |||||
| component="div" | |||||
| count={items.length} | |||||
| page={(pageNum - 1)} | |||||
| rowsPerPage={pageSize} | |||||
| onPageChange={onPageChange} | |||||
| onRowsPerPageChange={onPageSizeChange} | |||||
| rowsPerPageOptions={[10, 25, 50]} | |||||
| labelRowsPerPage={t("Rows per page")} | |||||
| labelDisplayedRows={({ from, to, count }) => | |||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||||
| } | |||||
| /> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default SearchResultsTable; | |||||
| @@ -0,0 +1,85 @@ | |||||
| import { Criterion } from "@/components/SearchBox/SearchBox"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { useState } from "react"; | |||||
| import { Card, CardContent, Typography, Grid, TextField, Button, Stack } from "@mui/material"; | |||||
| import { RestartAlt, Search } from "@mui/icons-material"; | |||||
| import { Autocomplete } from "@mui/material"; | |||||
| const VerticalSearchBox = ({ criteria, onSearch, onReset }: { | |||||
| criteria: Criterion<any>[]; | |||||
| onSearch: (inputs: Record<string, any>) => void; | |||||
| onReset?: () => void; | |||||
| }) => { | |||||
| const { t } = useTranslation("common"); | |||||
| const [inputs, setInputs] = useState<Record<string, any>>({}); | |||||
| const handleInputChange = (paramName: string, value: any) => { | |||||
| setInputs(prev => ({ ...prev, [paramName]: value })); | |||||
| }; | |||||
| const handleSearch = () => { | |||||
| onSearch(inputs); | |||||
| }; | |||||
| const handleReset = () => { | |||||
| setInputs({}); | |||||
| onReset?.(); | |||||
| }; | |||||
| return ( | |||||
| <Card> | |||||
| <CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}> | |||||
| <Typography variant="overline">{t("Search Criteria")}</Typography> | |||||
| <Grid container spacing={2} columns={{ xs: 12, sm: 12 }}> | |||||
| {criteria.map((c) => { | |||||
| return ( | |||||
| <Grid key={c.paramName} item xs={12}> | |||||
| {c.type === "text" && ( | |||||
| <TextField | |||||
| label={t(c.label)} | |||||
| fullWidth | |||||
| onChange={(e) => handleInputChange(c.paramName, e.target.value)} | |||||
| value={inputs[c.paramName] || ""} | |||||
| /> | |||||
| )} | |||||
| {c.type === "autocomplete" && ( | |||||
| <Autocomplete | |||||
| options={c.options || []} | |||||
| getOptionLabel={(option: any) => option.label} | |||||
| onChange={(_, value: any) => handleInputChange(c.paramName, value?.value || "")} | |||||
| value={c.options?.find(option => option.value === inputs[c.paramName]) || null} | |||||
| renderInput={(params) => ( | |||||
| <TextField | |||||
| {...params} | |||||
| label={t(c.label)} | |||||
| fullWidth | |||||
| /> | |||||
| )} | |||||
| /> | |||||
| )} | |||||
| </Grid> | |||||
| ); | |||||
| })} | |||||
| </Grid> | |||||
| <Stack direction="row" spacing={2} sx={{ mt: 2 }}> | |||||
| <Button | |||||
| variant="text" | |||||
| startIcon={<RestartAlt />} | |||||
| onClick={handleReset} | |||||
| > | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<Search />} | |||||
| onClick={handleSearch} | |||||
| > | |||||
| {t("Search")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default VerticalSearchBox; | |||||
| @@ -25,6 +25,7 @@ import { | |||||
| newassignPickOrder, | newassignPickOrder, | ||||
| AssignPickOrderInputs, | AssignPickOrderInputs, | ||||
| releaseAssignedPickOrders, | releaseAssignedPickOrders, | ||||
| fetchPickOrderWithStockClient, // Add this import | |||||
| } from "@/app/api/pickOrder/actions"; | } from "@/app/api/pickOrder/actions"; | ||||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | import { fetchNameList, NameList } from "@/app/api/user/actions"; | ||||
| import { | import { | ||||
| @@ -38,40 +39,36 @@ import dayjs from "dayjs"; | |||||
| import arraySupport from "dayjs/plugin/arraySupport"; | import arraySupport from "dayjs/plugin/arraySupport"; | ||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import { sortBy, uniqBy } from "lodash"; | import { sortBy, uniqBy } from "lodash"; | ||||
| import { fetchPickOrderItemsByPageClient } from "@/app/api/settings/item/actions"; | |||||
| import { createStockOutLine, CreateStockOutLine, fetchPickOrderDetails } from "@/app/api/pickOrder/actions"; | |||||
| dayjs.extend(arraySupport); | dayjs.extend(arraySupport); | ||||
| interface Props { | interface Props { | ||||
| filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
| } | } | ||||
| // 使用 fetchPickOrderItemsByPageClient 返回的数据结构 | |||||
| interface ItemRow { | |||||
| // Update the interface to match the new API response structure | |||||
| interface PickOrderRow { | |||||
| id: string; | id: string; | ||||
| pickOrderId: number; | |||||
| pickOrderCode: string; | |||||
| itemId: number; | |||||
| itemCode: string; | |||||
| itemName: string; | |||||
| requiredQty: number; | |||||
| currentStock: number; | |||||
| unit: string; | |||||
| targetDate: any; | |||||
| code: string; | |||||
| targetDate: string; | |||||
| type: string; | |||||
| status: string; | status: string; | ||||
| assignTo: number; | |||||
| groupName: string; | |||||
| consoCode?: string; | consoCode?: string; | ||||
| assignTo?: number; | |||||
| groupName?: string; | |||||
| pickOrderLines: PickOrderLineRow[]; | |||||
| } | } | ||||
| // 分组后的数据结构 | |||||
| interface GroupedItemRow { | |||||
| pickOrderId: number; | |||||
| pickOrderCode: string; | |||||
| targetDate: any; | |||||
| status: string; | |||||
| consoCode?: string; | |||||
| items: ItemRow[]; | |||||
| interface PickOrderLineRow { | |||||
| id: number; | |||||
| itemId: number; | |||||
| itemCode: string; | |||||
| itemName: string; | |||||
| availableQty: number | null; | |||||
| requiredQty: number; | |||||
| uomCode: string; | |||||
| uomDesc: string; | |||||
| suggestedList: any[]; | |||||
| } | } | ||||
| const style = { | const style = { | ||||
| @@ -89,10 +86,10 @@ const style = { | |||||
| const AssignTo: React.FC<Props> = ({ filterArgs }) => { | const AssignTo: React.FC<Props> = ({ filterArgs }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const { setIsUploading } = useUploadContext(); | const { setIsUploading } = useUploadContext(); | ||||
| // 修复:选择状态改为按 pick order ID 存储 | |||||
| const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<number[]>([]); | |||||
| const [filteredItems, setFilteredItems] = useState<ItemRow[]>([]); | |||||
| const [isUploading, setIsUploadingLocal] = useState(false); | |||||
| // Update state to use pick order data directly | |||||
| const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<string[]>([]); | |||||
| const [filteredPickOrders, setFilteredPickOrders] = useState<PickOrderRow[]>([]); | |||||
| const [isLoadingItems, setIsLoadingItems] = useState(false); | const [isLoadingItems, setIsLoadingItems] = useState(false); | ||||
| const [pagingController, setPagingController] = useState({ | const [pagingController, setPagingController] = useState({ | ||||
| pageNum: 1, | pageNum: 1, | ||||
| @@ -102,30 +99,13 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| const [modalOpen, setModalOpen] = useState(false); | const [modalOpen, setModalOpen] = useState(false); | ||||
| const [usernameList, setUsernameList] = useState<NameList[]>([]); | const [usernameList, setUsernameList] = useState<NameList[]>([]); | ||||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | ||||
| const [originalItemData, setOriginalItemData] = useState<ItemRow[]>([]); | |||||
| const [originalPickOrderData, setOriginalPickOrderData] = useState<PickOrderRow[]>([]); | |||||
| const formProps = useForm<AssignPickOrderInputs>(); | const formProps = useForm<AssignPickOrderInputs>(); | ||||
| const errors = formProps.formState.errors; | const errors = formProps.formState.errors; | ||||
| // 将项目按 pick order 分组 | |||||
| const groupedItems = useMemo(() => { | |||||
| const grouped = groupBy(filteredItems, 'pickOrderId'); | |||||
| return Object.entries(grouped).map(([pickOrderId, items]) => { | |||||
| const firstItem = items[0]; | |||||
| return { | |||||
| pickOrderId: parseInt(pickOrderId), | |||||
| pickOrderCode: firstItem.pickOrderCode, | |||||
| targetDate: firstItem.targetDate, | |||||
| status: firstItem.status, | |||||
| consoCode: firstItem.consoCode, | |||||
| items: items, | |||||
| groupName: firstItem.groupName, | |||||
| } as GroupedItemRow; | |||||
| }); | |||||
| }, [filteredItems]); | |||||
| // 修复:处理 pick order 选择 | |||||
| const handlePickOrderSelect = useCallback((pickOrderId: number, checked: boolean) => { | |||||
| // Update the handler functions to work with string IDs | |||||
| const handlePickOrderSelect = useCallback((pickOrderId: string, checked: boolean) => { | |||||
| if (checked) { | if (checked) { | ||||
| setSelectedPickOrderIds(prev => [...prev, pickOrderId]); | setSelectedPickOrderIds(prev => [...prev, pickOrderId]); | ||||
| } else { | } else { | ||||
| @@ -133,62 +113,50 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| } | } | ||||
| }, []); | }, []); | ||||
| // 修复:检查 pick order 是否被选中 | |||||
| const isPickOrderSelected = useCallback((pickOrderId: number) => { | |||||
| const isPickOrderSelected = useCallback((pickOrderId: string) => { | |||||
| return selectedPickOrderIds.includes(pickOrderId); | return selectedPickOrderIds.includes(pickOrderId); | ||||
| }, [selectedPickOrderIds]); | }, [selectedPickOrderIds]); | ||||
| // 使用 fetchPickOrderItemsByPageClient 获取数据 | |||||
| // Update the fetch function to use the correct endpoint | |||||
| const fetchNewPageItems = useCallback( | const fetchNewPageItems = useCallback( | ||||
| async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => { | async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => { | ||||
| console.log("=== fetchNewPageItems called ==="); | |||||
| console.log("pagingController:", pagingController); | |||||
| console.log("filterArgs:", filterArgs); | |||||
| setIsLoadingItems(true); | setIsLoadingItems(true); | ||||
| try { | try { | ||||
| const params = { | const params = { | ||||
| ...pagingController, | ...pagingController, | ||||
| ...filterArgs, | ...filterArgs, | ||||
| // 新增:只获取状态为 "assigned" 的提料单 | |||||
| pageNum: (pagingController.pageNum || 1) - 1, | |||||
| pageSize: pagingController.pageSize || 10, | |||||
| // Filter for assigned status only | |||||
| status: "assigned" | status: "assigned" | ||||
| }; | }; | ||||
| console.log("Final params:", params); | |||||
| const res = await fetchPickOrderItemsByPageClient(params); | |||||
| console.log("API Response:", res); | |||||
| const res = await fetchPickOrderWithStockClient(params); | |||||
| if (res && res.records) { | if (res && res.records) { | ||||
| console.log("Records received:", res.records.length); | |||||
| console.log("First record:", res.records[0]); | |||||
| const itemRows: ItemRow[] = res.records.map((item: any) => ({ | |||||
| id: item.id, | |||||
| pickOrderId: item.pickOrderId, | |||||
| pickOrderCode: item.pickOrderCode, | |||||
| itemId: item.itemId, | |||||
| itemCode: item.itemCode, | |||||
| itemName: item.itemName, | |||||
| requiredQty: item.requiredQty, | |||||
| currentStock: item.currentStock ?? 0, | |||||
| unit: item.unit, | |||||
| targetDate: item.targetDate, | |||||
| status: item.status, | |||||
| consoCode: item.consoCode, | |||||
| assignTo: item.assignTo, | |||||
| // Convert pick order data to the expected format | |||||
| const pickOrderRows: PickOrderRow[] = res.records.map((pickOrder: any) => ({ | |||||
| id: pickOrder.id, | |||||
| code: pickOrder.code, | |||||
| targetDate: pickOrder.targetDate, | |||||
| type: pickOrder.type, | |||||
| status: pickOrder.status, | |||||
| assignTo: pickOrder.assignTo, | |||||
| groupName: pickOrder.groupName || "No Group", | |||||
| consoCode: pickOrder.consoCode, | |||||
| pickOrderLines: pickOrder.pickOrderLines || [] | |||||
| })); | })); | ||||
| setOriginalItemData(itemRows); | |||||
| setFilteredItems(itemRows); | |||||
| setOriginalPickOrderData(pickOrderRows); | |||||
| setFilteredPickOrders(pickOrderRows); | |||||
| setTotalCountItems(res.total); | setTotalCountItems(res.total); | ||||
| } else { | } else { | ||||
| console.log("No records in response"); | |||||
| setFilteredItems([]); | |||||
| setFilteredPickOrders([]); | |||||
| setTotalCountItems(0); | setTotalCountItems(0); | ||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Error fetching items:", error); | |||||
| setFilteredItems([]); | |||||
| console.error("Error fetching pick orders:", error); | |||||
| setFilteredPickOrders([]); | |||||
| setTotalCountItems(0); | setTotalCountItems(0); | ||||
| } finally { | } finally { | ||||
| setIsLoadingItems(false); | setIsLoadingItems(false); | ||||
| @@ -197,47 +165,91 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| [], | [], | ||||
| ); | ); | ||||
| // 新增:处理 Release 操作(包含完整的库存管理) | |||||
| const handleRelease = useCallback(async () => { | |||||
| if (selectedPickOrderIds.length === 0) return; | |||||
| setIsUploading(true); | |||||
| try { | |||||
| // 调用新的 release API,包含完整的库存管理功能 | |||||
| const releaseRes = await releaseAssignedPickOrders({ | |||||
| pickOrderIds: selectedPickOrderIds, | |||||
| assignTo: 0, // 这个参数在 release 时不会被使用 | |||||
| }); | |||||
| if (releaseRes && releaseRes.code === "SUCCESS") { | |||||
| console.log("Release successful with inventory management:", releaseRes); | |||||
| setSelectedPickOrderIds([]); // 清空选择 | |||||
| fetchNewPageItems(pagingController, filterArgs); | |||||
| } else { | |||||
| console.error("Release failed:", releaseRes); | |||||
| // Handle Release operation | |||||
| // Handle Release operation | |||||
| const handleRelease = useCallback(async () => { | |||||
| if (selectedPickOrderIds.length === 0) return; | |||||
| setIsUploading(true); | |||||
| try { | |||||
| // Get the assigned user from the selected pick orders | |||||
| const selectedPickOrders = filteredPickOrders.filter(pickOrder => | |||||
| selectedPickOrderIds.includes(pickOrder.id) | |||||
| ); | |||||
| // Check if all selected pick orders have the same assigned user | |||||
| const assignedUsers = selectedPickOrders.map(po => po.assignTo).filter(Boolean); | |||||
| if (assignedUsers.length === 0) { | |||||
| alert("Selected pick orders are not assigned to any user."); | |||||
| return; | |||||
| } | |||||
| const assignToValue = assignedUsers[0]; | |||||
| // Validate that all pick orders are assigned to the same user | |||||
| const allSameUser = assignedUsers.every(userId => userId === assignToValue); | |||||
| if (!allSameUser) { | |||||
| alert("All selected pick orders must be assigned to the same user."); | |||||
| return; | |||||
| } | |||||
| console.log("Using assigned user:", assignToValue); | |||||
| console.log("selectedPickOrderIds:", selectedPickOrderIds); | |||||
| const releaseRes = await releaseAssignedPickOrders({ | |||||
| pickOrderIds: selectedPickOrderIds.map(id => parseInt(id)), | |||||
| assignTo: assignToValue | |||||
| }); | |||||
| if (releaseRes.code === "SUCCESS") { | |||||
| console.log("Pick orders released successfully"); | |||||
| // Get the consoCode from the response | |||||
| const consoCode = (releaseRes.entity as any)?.consoCode; | |||||
| if (consoCode) { | |||||
| // Create StockOutLine records for each pick order line | |||||
| for (const pickOrder of selectedPickOrders) { | |||||
| for (const line of pickOrder.pickOrderLines) { | |||||
| try { | |||||
| const stockOutLineData = { | |||||
| consoCode: consoCode, | |||||
| pickOrderLineId: line.id, | |||||
| inventoryLotLineId: 0, // This will be set when user scans QR code | |||||
| qty: line.requiredQty, | |||||
| }; | |||||
| console.log("Creating stock out line:", stockOutLineData); | |||||
| await createStockOutLine(stockOutLineData); | |||||
| } catch (error) { | |||||
| console.error("Error creating stock out line for line", line.id, error); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } catch (error) { | |||||
| console.error("Error in release:", error); | |||||
| } finally { | |||||
| setIsUploading(false); | |||||
| fetchNewPageItems(pagingController, filterArgs); | |||||
| } else { | |||||
| console.error("Release failed:", releaseRes.message); | |||||
| } | } | ||||
| }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); | |||||
| } catch (error) { | |||||
| console.error("Error releasing pick orders:", error); | |||||
| } finally { | |||||
| setIsUploading(false); | |||||
| } | |||||
| }, [selectedPickOrderIds, filteredPickOrders, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); | |||||
| // Update search criteria to match the new data structure | |||||
| const searchCriteria: Criterion<any>[] = useMemo( | const searchCriteria: Criterion<any>[] = useMemo( | ||||
| () => [ | () => [ | ||||
| { | { | ||||
| label: t("Pick Order Code"), | label: t("Pick Order Code"), | ||||
| paramName: "pickOrderCode", | |||||
| paramName: "code", | |||||
| type: "text", | type: "text", | ||||
| }, | }, | ||||
| { | |||||
| label: t("Item Code"), | |||||
| paramName: "itemCode", | |||||
| type: "text" | |||||
| }, | |||||
| { | { | ||||
| label: t("Item Name"), | |||||
| paramName: "itemName", | |||||
| label: t("Group Code"), | |||||
| paramName: "groupName", | |||||
| type: "text", | type: "text", | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -250,72 +262,64 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| [t], | [t], | ||||
| ); | ); | ||||
| // Update search function to work with pick order data | |||||
| const handleSearch = useCallback((query: Record<string, any>) => { | const handleSearch = useCallback((query: Record<string, any>) => { | ||||
| setSearchQuery({ ...query }); | setSearchQuery({ ...query }); | ||||
| console.log("Search query:", query); | |||||
| const filtered = originalItemData.filter((item) => { | |||||
| const itemTargetDateStr = arrayToDayjs(item.targetDate); | |||||
| const filtered = originalPickOrderData.filter((pickOrder) => { | |||||
| const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate); | |||||
| const itemCodeMatch = !query.itemCode || | |||||
| item.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); | |||||
| const itemNameMatch = !query.itemName || | |||||
| item.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); | |||||
| const codeMatch = !query.code || | |||||
| pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase()); | |||||
| const pickOrderCodeMatch = !query.pickOrderCode || | |||||
| item.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); | |||||
| const groupNameMatch = !query.groupName || | |||||
| pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); | |||||
| // 日期范围搜索 | |||||
| // Date range search | |||||
| let dateMatch = true; | let dateMatch = true; | ||||
| if (query.targetDate || query.targetDateTo) { | if (query.targetDate || query.targetDateTo) { | ||||
| try { | try { | ||||
| if (query.targetDate && !query.targetDateTo) { | if (query.targetDate && !query.targetDateTo) { | ||||
| const fromDate = dayjs(query.targetDate); | const fromDate = dayjs(query.targetDate); | ||||
| dateMatch = itemTargetDateStr.isSame(fromDate, 'day') || | |||||
| itemTargetDateStr.isAfter(fromDate, 'day'); | |||||
| dateMatch = pickOrderTargetDateStr.isSame(fromDate, 'day') || | |||||
| pickOrderTargetDateStr.isAfter(fromDate, 'day'); | |||||
| } else if (!query.targetDate && query.targetDateTo) { | } else if (!query.targetDate && query.targetDateTo) { | ||||
| const toDate = dayjs(query.targetDateTo); | const toDate = dayjs(query.targetDateTo); | ||||
| dateMatch = itemTargetDateStr.isSame(toDate, 'day') || | |||||
| itemTargetDateStr.isBefore(toDate, 'day'); | |||||
| dateMatch = pickOrderTargetDateStr.isSame(toDate, 'day') || | |||||
| pickOrderTargetDateStr.isBefore(toDate, 'day'); | |||||
| } else if (query.targetDate && query.targetDateTo) { | } else if (query.targetDate && query.targetDateTo) { | ||||
| const fromDate = dayjs(query.targetDate); | const fromDate = dayjs(query.targetDate); | ||||
| const toDate = dayjs(query.targetDateTo); | const toDate = dayjs(query.targetDateTo); | ||||
| dateMatch = (itemTargetDateStr.isSame(fromDate, 'day') || | |||||
| itemTargetDateStr.isAfter(fromDate, 'day')) && | |||||
| (itemTargetDateStr.isSame(toDate, 'day') || | |||||
| itemTargetDateStr.isBefore(toDate, 'day')); | |||||
| dateMatch = (pickOrderTargetDateStr.isSame(fromDate, 'day') || | |||||
| pickOrderTargetDateStr.isAfter(fromDate, 'day')) && | |||||
| (pickOrderTargetDateStr.isSame(toDate, 'day') || | |||||
| pickOrderTargetDateStr.isBefore(toDate, 'day')); | |||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| console.error("Date parsing error:", error); | console.error("Date parsing error:", error); | ||||
| dateMatch = true; | dateMatch = true; | ||||
| } | } | ||||
| } | } | ||||
| const statusMatch = !query.status || | |||||
| query.status.toLowerCase() === "all" || | |||||
| item.status?.toLowerCase().includes((query.status || "").toLowerCase()); | |||||
| return itemCodeMatch && itemNameMatch && pickOrderCodeMatch && dateMatch && statusMatch; | |||||
| return codeMatch && groupNameMatch && dateMatch; | |||||
| }); | }); | ||||
| console.log("Filtered items count:", filtered.length); | |||||
| setFilteredItems(filtered); | |||||
| }, [originalItemData]); | |||||
| setFilteredPickOrders(filtered); | |||||
| }, [originalPickOrderData]); | |||||
| const handleReset = useCallback(() => { | const handleReset = useCallback(() => { | ||||
| setSearchQuery({}); | setSearchQuery({}); | ||||
| setFilteredItems(originalItemData); | |||||
| setFilteredPickOrders(originalPickOrderData); | |||||
| setTimeout(() => { | setTimeout(() => { | ||||
| setSearchQuery({}); | setSearchQuery({}); | ||||
| }, 0); | }, 0); | ||||
| }, [originalItemData]); | |||||
| }, [originalPickOrderData]); | |||||
| // 修复:处理分页变化 | |||||
| // Pagination handlers | |||||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | const handlePageChange = useCallback((event: unknown, newPage: number) => { | ||||
| const newPagingController = { | const newPagingController = { | ||||
| ...pagingController, | ...pagingController, | ||||
| pageNum: newPage + 1, // API 使用 1-based 分页 | |||||
| pageNum: newPage + 1, | |||||
| }; | }; | ||||
| setPagingController(newPagingController); | setPagingController(newPagingController); | ||||
| }, [pagingController]); | }, [pagingController]); | ||||
| @@ -323,52 +327,19 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | ||||
| const newPageSize = parseInt(event.target.value, 10); | const newPageSize = parseInt(event.target.value, 10); | ||||
| const newPagingController = { | const newPagingController = { | ||||
| pageNum: 1, // 重置到第一页 | |||||
| pageNum: 1, | |||||
| pageSize: newPageSize, | pageSize: newPageSize, | ||||
| }; | }; | ||||
| setPagingController(newPagingController); | setPagingController(newPagingController); | ||||
| }, []); | }, []); | ||||
| const handleAssignAndRelease = useCallback(async (data: AssignPickOrderInputs) => { | |||||
| if (selectedPickOrderIds.length === 0) return; | |||||
| setIsUploading(true); | |||||
| try { | |||||
| // 修复:直接使用选中的 pick order IDs | |||||
| const assignRes = await newassignPickOrder({ | |||||
| pickOrderIds: selectedPickOrderIds, | |||||
| assignTo: data.assignTo, | |||||
| }); | |||||
| if (assignRes && assignRes.code === "SUCCESS") { | |||||
| console.log("Assign successful:", assignRes); | |||||
| setModalOpen(false); | |||||
| setSelectedPickOrderIds([]); // 清空选择 | |||||
| fetchNewPageItems(pagingController, filterArgs); | |||||
| } else { | |||||
| console.error("Assign failed:", assignRes); | |||||
| } | |||||
| } catch (error) { | |||||
| console.error("Error in assign:", error); | |||||
| } finally { | |||||
| setIsUploading(false); | |||||
| } | |||||
| }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); | |||||
| const openAssignModal = useCallback(() => { | |||||
| setModalOpen(true); | |||||
| formProps.reset(); | |||||
| }, [formProps]); | |||||
| // 组件挂载时加载数据 | |||||
| // Component mount effect | |||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log("=== Component mounted ==="); | |||||
| fetchNewPageItems(pagingController, filterArgs || {}); | fetchNewPageItems(pagingController, filterArgs || {}); | ||||
| }, []); // 只在组件挂载时执行一次 | |||||
| }, []); | |||||
| // 当 pagingController 或 filterArgs 变化时重新调用 API | |||||
| // Dependencies change effect | |||||
| useEffect(() => { | useEffect(() => { | ||||
| console.log("=== Dependencies changed ==="); | |||||
| if (pagingController && (filterArgs || {})) { | if (pagingController && (filterArgs || {})) { | ||||
| fetchNewPageItems(pagingController, filterArgs || {}); | fetchNewPageItems(pagingController, filterArgs || {}); | ||||
| } | } | ||||
| @@ -388,9 +359,9 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| loadUsernameList(); | loadUsernameList(); | ||||
| }, []); | }, []); | ||||
| // 自定义分组表格组件 | |||||
| const CustomGroupedTable = () => { | |||||
| // 获取用户名的辅助函数 | |||||
| // Update the table component to work with pick order data directly | |||||
| const CustomPickOrderTable = () => { | |||||
| // Helper function to get user name | |||||
| const getUserName = useCallback((assignToId: number | null | undefined) => { | const getUserName = useCallback((assignToId: number | null | undefined) => { | ||||
| if (!assignToId) return '-'; | if (!assignToId) return '-'; | ||||
| const user = usernameList.find(u => u.id === assignToId); | const user = usernameList.find(u => u.id === assignToId); | ||||
| @@ -405,7 +376,7 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell>{t("Selected")}</TableCell> | <TableCell>{t("Selected")}</TableCell> | ||||
| <TableCell>{t("Pick Order Code")}</TableCell> | <TableCell>{t("Pick Order Code")}</TableCell> | ||||
| <TableCell>{t("Group Name")}</TableCell> | |||||
| <TableCell>{t("Group Code")}</TableCell> | |||||
| <TableCell>{t("Item Code")}</TableCell> | <TableCell>{t("Item Code")}</TableCell> | ||||
| <TableCell>{t("Item Name")}</TableCell> | <TableCell>{t("Item Name")}</TableCell> | ||||
| <TableCell align="right">{t("Order Quantity")}</TableCell> | <TableCell align="right">{t("Order Quantity")}</TableCell> | ||||
| @@ -416,75 +387,72 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| </TableRow> | </TableRow> | ||||
| </TableHead> | </TableHead> | ||||
| <TableBody> | <TableBody> | ||||
| {groupedItems.length === 0 ? ( | |||||
| {filteredPickOrders.length === 0 ? ( | |||||
| <TableRow> | <TableRow> | ||||
| <TableCell colSpan={9} align="center"> | |||||
| <TableCell colSpan={10} align="center"> | |||||
| <Typography variant="body2" color="text.secondary"> | <Typography variant="body2" color="text.secondary"> | ||||
| {t("No data available")} | {t("No data available")} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| ) : ( | ) : ( | ||||
| groupedItems.map((group) => ( | |||||
| group.items.map((item, index) => ( | |||||
| <TableRow key={item.id}> | |||||
| {/* Checkbox - 只在第一个项目显示,按 pick order 选择 */} | |||||
| filteredPickOrders.map((pickOrder) => ( | |||||
| pickOrder.pickOrderLines.map((line: PickOrderLineRow, index: number) => ( | |||||
| <TableRow key={`${pickOrder.id}-${line.id}`}> | |||||
| {/* Checkbox - only show for first line of each pick order */} | |||||
| <TableCell> | <TableCell> | ||||
| {index === 0 ? ( | {index === 0 ? ( | ||||
| <Checkbox | <Checkbox | ||||
| checked={isPickOrderSelected(group.pickOrderId)} | |||||
| onChange={(e) => handlePickOrderSelect(group.pickOrderId, e.target.checked)} | |||||
| disabled={!isEmpty(item.consoCode)} | |||||
| checked={isPickOrderSelected(pickOrder.id)} | |||||
| onChange={(e) => handlePickOrderSelect(pickOrder.id, e.target.checked)} | |||||
| disabled={!isEmpty(pickOrder.consoCode)} | |||||
| /> | /> | ||||
| ) : null} | ) : null} | ||||
| </TableCell> | </TableCell> | ||||
| {/* Pick Order Code - 只在第一个项目显示 */} | |||||
| {/* Pick Order Code - only show for first line */} | |||||
| <TableCell> | <TableCell> | ||||
| {index === 0 ? item.pickOrderCode : null} | |||||
| {index === 0 ? pickOrder.code : null} | |||||
| </TableCell> | </TableCell> | ||||
| {/* Group Name */} | |||||
| {/* Group Name - only show for first line */} | |||||
| <TableCell> | <TableCell> | ||||
| {index === 0 ? (item.groupName || "No Group") : null} | |||||
| {index === 0 ? pickOrder.groupName : null} | |||||
| </TableCell> | </TableCell> | ||||
| {/* Item Code */} | {/* Item Code */} | ||||
| <TableCell>{item.itemCode}</TableCell> | |||||
| <TableCell>{line.itemCode}</TableCell> | |||||
| {/* Item Name */} | {/* Item Name */} | ||||
| <TableCell>{item.itemName}</TableCell> | |||||
| <TableCell>{line.itemName}</TableCell> | |||||
| {/* Order Quantity */} | {/* Order Quantity */} | ||||
| <TableCell align="right">{item.requiredQty}</TableCell> | |||||
| <TableCell align="right">{line.requiredQty}</TableCell> | |||||
| {/* Current Stock */} | {/* Current Stock */} | ||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Typography | <Typography | ||||
| variant="body2" | variant="body2" | ||||
| color={item.currentStock > 0 ? "success.main" : "error.main"} | |||||
| sx={{ fontWeight: item.currentStock > 0 ? 'bold' : 'normal' }} | |||||
| color={line.availableQty && line.availableQty > 0 ? "success.main" : "error.main"} | |||||
| sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }} | |||||
| > | > | ||||
| {item.currentStock.toLocaleString()} | |||||
| {(line.availableQty || 0).toLocaleString()} | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Unit */} | {/* Unit */} | ||||
| <TableCell align="right">{item.unit}</TableCell> | |||||
| <TableCell align="right">{line.uomDesc}</TableCell> | |||||
| {/* Target Date - 只在第一个项目显示 */} | |||||
| {/* Target Date - only show for first line */} | |||||
| <TableCell> | <TableCell> | ||||
| {index === 0 ? ( | {index === 0 ? ( | ||||
| arrayToDayjs(item.targetDate) | |||||
| arrayToDayjs(pickOrder.targetDate) | |||||
| .add(-1, "month") | .add(-1, "month") | ||||
| .format(OUTPUT_DATE_FORMAT) | .format(OUTPUT_DATE_FORMAT) | ||||
| ) : null} | ) : null} | ||||
| </TableCell> | </TableCell> | ||||
| {/* Assigned To - 只在第一个项目显示,显示用户名 */} | |||||
| {/* Assigned To - only show for first line */} | |||||
| <TableCell> | <TableCell> | ||||
| {index === 0 ? ( | {index === 0 ? ( | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| {getUserName(item.assignTo)} | |||||
| {getUserName(pickOrder.assignTo)} | |||||
| </Typography> | </Typography> | ||||
| ) : null} | ) : null} | ||||
| </TableCell> | </TableCell> | ||||
| @@ -496,15 +464,14 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| </Table> | </Table> | ||||
| </TableContainer> | </TableContainer> | ||||
| {/* 修复:添加分页组件 */} | |||||
| <TablePagination | <TablePagination | ||||
| component="div" | component="div" | ||||
| count={totalCountItems || 0} | count={totalCountItems || 0} | ||||
| page={(pagingController.pageNum - 1)} // 转换为 0-based | |||||
| page={(pagingController.pageNum - 1)} | |||||
| rowsPerPage={pagingController.pageSize} | rowsPerPage={pagingController.pageSize} | ||||
| onPageChange={handlePageChange} | onPageChange={handlePageChange} | ||||
| onRowsPerPageChange={handlePageSizeChange} | onRowsPerPageChange={handlePageSizeChange} | ||||
| rowsPerPageOptions={[10, 25, 50]} | |||||
| rowsPerPageOptions={[10, 25, 50, 100]} | |||||
| labelRowsPerPage={t("Rows per page")} | labelRowsPerPage={t("Rows per page")} | ||||
| labelDisplayedRows={({ from, to, count }) => | labelDisplayedRows={({ from, to, count }) => | ||||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | ||||
| @@ -522,7 +489,7 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| {isLoadingItems ? ( | {isLoadingItems ? ( | ||||
| <CircularProgress size={40} /> | <CircularProgress size={40} /> | ||||
| ) : ( | ) : ( | ||||
| <CustomGroupedTable /> | |||||
| <CustomPickOrderTable /> | |||||
| )} | )} | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| @@ -37,7 +37,9 @@ import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
| import SearchResults, { Column } from "../SearchResults/SearchResults"; | import SearchResults, { Column } from "../SearchResults/SearchResults"; | ||||
| import { fetchJobOrderDetailByCode } from "@/app/api/jo/actions"; | import { fetchJobOrderDetailByCode } from "@/app/api/jo/actions"; | ||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import VerticalSearchBox from "./VerticalSearchBox"; | |||||
| import SearchResultsTable from './SearchResultsTable'; | |||||
| import CreatedItemsTable from './CreatedItemsTable'; | |||||
| type Props = { | type Props = { | ||||
| filterArgs?: Record<string, any>; | filterArgs?: Record<string, any>; | ||||
| searchQuery?: Record<string, any>; | searchQuery?: Record<string, any>; | ||||
| @@ -88,8 +90,11 @@ interface JobOrderDetailPickLine { | |||||
| interface Group { | interface Group { | ||||
| id: number; | id: number; | ||||
| name: string; | name: string; | ||||
| targetDate: string; | |||||
| targetDate: string ; | |||||
| } | } | ||||
| // Move the counter outside the component to persist across re-renders | |||||
| let checkboxChangeCallCount = 0; | |||||
| let processingItems = new Set<number>(); | |||||
| const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCreated }) => { | const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCreated }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| @@ -217,11 +222,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| ); | ); | ||||
| const handleSearch = useCallback(() => { | const handleSearch = useCallback(() => { | ||||
| if (!type) { | |||||
| alert(t("Please select type")); | |||||
| return; | |||||
| } | |||||
| if (!searchCode && !searchName) { | if (!searchCode && !searchName) { | ||||
| alert(t("Please enter at least code or name")); | alert(t("Please enter at least code or name")); | ||||
| return; | return; | ||||
| @@ -368,7 +369,12 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| // Update the handleGroupTargetDateChange function to update selected items that belong to that group | // Update the handleGroupTargetDateChange function to update selected items that belong to that group | ||||
| const handleGroupTargetDateChange = useCallback((groupId: number, newTargetDate: string) => { | const handleGroupTargetDateChange = useCallback((groupId: number, newTargetDate: string) => { | ||||
| setGroups(prev => prev.map(g => (g.id === groupId ? { ...g, targetDate: newTargetDate } : g))); | setGroups(prev => prev.map(g => (g.id === groupId ? { ...g, targetDate: newTargetDate } : g))); | ||||
| setSelectedGroup(prev => { | |||||
| if (prev && prev.id === groupId) { | |||||
| return { ...prev, targetDate: newTargetDate }; | |||||
| } | |||||
| return prev; | |||||
| }); | |||||
| // Update selected items that belong to this group | // Update selected items that belong to this group | ||||
| setSecondSearchResults(prev => prev.map(item => | setSecondSearchResults(prev => prev.map(item => | ||||
| item.groupId === groupId | item.groupId === groupId | ||||
| @@ -378,6 +384,14 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| } | } | ||||
| : item | : item | ||||
| )); | )); | ||||
| setCreatedItems(prev => prev.map(item => | |||||
| item.groupId === groupId | |||||
| ? { | |||||
| ...item, | |||||
| targetDate: newTargetDate | |||||
| } | |||||
| : item | |||||
| )); | |||||
| }, []); | }, []); | ||||
| // Fix the handleCreateGroup function to use the API properly | // Fix the handleCreateGroup function to use the API properly | ||||
| @@ -390,7 +404,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| const newGroup: Group = { | const newGroup: Group = { | ||||
| id: response.id, | id: response.id, | ||||
| name: response.name, | name: response.name, | ||||
| targetDate: dayjs().format(INPUT_DATE_FORMAT) | |||||
| targetDate: "" | |||||
| }; | }; | ||||
| setGroups(prev => [...prev, newGroup]); | setGroups(prev => [...prev, newGroup]); | ||||
| @@ -405,7 +419,94 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| alert(t('Failed to create group')); | alert(t('Failed to create group')); | ||||
| } | } | ||||
| }, [t]); | }, [t]); | ||||
| const checkAndAutoAddItem = useCallback((itemId: number) => { | |||||
| const item = secondSearchResults.find(i => i.id === itemId); | |||||
| if (!item) return; | |||||
| // Check if item has ALL 3 conditions: | |||||
| // 1. Item is selected (checkbox checked) | |||||
| const isSelected = selectedSecondSearchItemIds.includes(itemId); | |||||
| // 2. Group is assigned | |||||
| const hasGroup = item.groupId !== undefined && item.groupId !== null; | |||||
| // 3. Quantity is entered | |||||
| const hasQty = item.qty !== null && item.qty !== undefined && item.qty > 0; | |||||
| if (isSelected && hasGroup && hasQty && !isItemInCreated(item.id)) { | |||||
| // Auto-add to created items | |||||
| const newCreatedItem: CreatedItem = { | |||||
| itemId: item.id, | |||||
| itemName: item.label, | |||||
| itemCode: item.label, | |||||
| qty: item.qty || 1, | |||||
| uom: item.uom || "", | |||||
| uomId: item.uomId || 0, | |||||
| uomDesc: item.uomDesc || "", | |||||
| isSelected: true, | |||||
| currentStockBalance: item.currentStockBalance, | |||||
| targetDate: item.targetDate || targetDate, | |||||
| groupId: item.groupId || undefined, | |||||
| }; | |||||
| setCreatedItems(prev => [...prev, newCreatedItem]); | |||||
| // Remove from search results since it's now in created items | |||||
| setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId)); | |||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); | |||||
| } | |||||
| }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); | |||||
| // Add this function after checkAndAutoAddItem | |||||
| // Add this function after checkAndAutoAddItem | |||||
| const handleQtyBlur = useCallback((itemId: number) => { | |||||
| // Only auto-add if item is already selected (scenario 1: select first, then enter quantity) | |||||
| setTimeout(() => { | |||||
| const currentItem = secondSearchResults.find(i => i.id === itemId); | |||||
| if (!currentItem) return; | |||||
| const isSelected = selectedSecondSearchItemIds.includes(itemId); | |||||
| const hasGroup = currentItem.groupId !== undefined && currentItem.groupId !== null; | |||||
| const hasQty = currentItem.qty !== null && currentItem.qty !== undefined && currentItem.qty > 0; | |||||
| // Only auto-add if item is already selected (scenario 1: select first, then enter quantity) | |||||
| if (isSelected && hasGroup && hasQty && !isItemInCreated(currentItem.id)) { | |||||
| const newCreatedItem: CreatedItem = { | |||||
| itemId: currentItem.id, | |||||
| itemName: currentItem.label, | |||||
| itemCode: currentItem.label, | |||||
| qty: currentItem.qty || 1, | |||||
| uom: currentItem.uom || "", | |||||
| uomId: currentItem.uomId || 0, | |||||
| uomDesc: currentItem.uomDesc || "", | |||||
| isSelected: true, | |||||
| currentStockBalance: currentItem.currentStockBalance, | |||||
| targetDate: currentItem.targetDate || targetDate, | |||||
| groupId: currentItem.groupId || undefined, | |||||
| }; | |||||
| setCreatedItems(prev => [...prev, newCreatedItem]); | |||||
| setSecondSearchResults(prev => prev.filter(searchItem => searchItem.id !== itemId)); | |||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); | |||||
| } | |||||
| }, 0); | |||||
| }, [secondSearchResults, selectedSecondSearchItemIds, isItemInCreated, targetDate]); | |||||
| const handleSearchItemGroupChange = useCallback((itemId: number, groupId: string) => { | |||||
| const gid = groupId ? Number(groupId) : undefined; | |||||
| const group = groups.find(g => g.id === gid); | |||||
| setSecondSearchResults(prev => prev.map(item => | |||||
| item.id === itemId | |||||
| ? { | |||||
| ...item, | |||||
| groupId: gid, | |||||
| targetDate: group?.targetDate || undefined | |||||
| } | |||||
| : item | |||||
| )); | |||||
| // Check auto-add after group assignment | |||||
| setTimeout(() => { | |||||
| checkAndAutoAddItem(itemId); | |||||
| }, 0); | |||||
| }, [groups, checkAndAutoAddItem]); | |||||
| // 5) 选中新增的待选项:依然按“当前 Group”赋 groupId + targetDate(新加入的应随 Group) | // 5) 选中新增的待选项:依然按“当前 Group”赋 groupId + targetDate(新加入的应随 Group) | ||||
| const handleSecondSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => { | const handleSecondSearchItemSelect = useCallback((itemId: number, isSelected: boolean) => { | ||||
| if (!isSelected) return; | if (!isSelected) return; | ||||
| @@ -444,18 +545,13 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| alert(t("Please select at least one item to submit")); | alert(t("Please select at least one item to submit")); | ||||
| return; | return; | ||||
| } | } | ||||
| if (!data.type) { | |||||
| alert(t("Please select product type")); | |||||
| return; | |||||
| } | |||||
| // Remove the data.targetDate check since we'll use group target dates | |||||
| // if (!data.targetDate) { | |||||
| // alert(t("Please select target date")); | |||||
| // ✅ 修复:自动填充 type 为 "Consumable",不再强制用户选择 | |||||
| // if (!data.type) { | |||||
| // alert(t("Please select product type")); | |||||
| // return; | // return; | ||||
| // } | // } | ||||
| // 按组分组选中的项目 | // 按组分组选中的项目 | ||||
| const itemsByGroup = selectedCreatedItems.reduce((acc, item) => { | const itemsByGroup = selectedCreatedItems.reduce((acc, item) => { | ||||
| const groupId = item.groupId || 'no-group'; | const groupId = item.groupId || 'no-group'; | ||||
| @@ -465,13 +561,13 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| acc[groupId].push(item); | acc[groupId].push(item); | ||||
| return acc; | return acc; | ||||
| }, {} as Record<string | number, typeof selectedCreatedItems>); | }, {} as Record<string | number, typeof selectedCreatedItems>); | ||||
| console.log("Items grouped by group:", itemsByGroup); | console.log("Items grouped by group:", itemsByGroup); | ||||
| let successCount = 0; | let successCount = 0; | ||||
| const totalGroups = Object.keys(itemsByGroup).length; | const totalGroups = Object.keys(itemsByGroup).length; | ||||
| const groupUpdates: Array<{groupId: number, pickOrderId: number}> = []; | const groupUpdates: Array<{groupId: number, pickOrderId: number}> = []; | ||||
| // 为每个组创建提料单 | // 为每个组创建提料单 | ||||
| for (const [groupId, items] of Object.entries(itemsByGroup)) { | for (const [groupId, items] of Object.entries(itemsByGroup)) { | ||||
| try { | try { | ||||
| @@ -492,9 +588,9 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| if (!groupTargetDate) { | if (!groupTargetDate) { | ||||
| groupTargetDate = dayjs().format(INPUT_DATE_FORMAT); | groupTargetDate = dayjs().format(INPUT_DATE_FORMAT); | ||||
| } | } | ||||
| console.log(`Creating pick order for group: ${groupName} with ${items.length} items, target date: ${groupTargetDate}`); | console.log(`Creating pick order for group: ${groupName} with ${items.length} items, target date: ${groupTargetDate}`); | ||||
| let formattedTargetDate = groupTargetDate; | let formattedTargetDate = groupTargetDate; | ||||
| if (groupTargetDate && typeof groupTargetDate === 'string') { | if (groupTargetDate && typeof groupTargetDate === 'string') { | ||||
| try { | try { | ||||
| @@ -506,9 +602,10 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| return; | return; | ||||
| } | } | ||||
| } | } | ||||
| // ✅ 修复:自动使用 "Consumable" 作为默认 type | |||||
| const pickOrderData: SavePickOrderRequest = { | const pickOrderData: SavePickOrderRequest = { | ||||
| type: data.type || "Consumable", | |||||
| type: data.type || "Consumable", // 如果用户选择了 type 就用用户的,否则默认 "Consumable" | |||||
| targetDate: formattedTargetDate, | targetDate: formattedTargetDate, | ||||
| pickOrderLine: items.map(item => ({ | pickOrderLine: items.map(item => ({ | ||||
| itemId: item.itemId, | itemId: item.itemId, | ||||
| @@ -516,7 +613,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| uomId: item.uomId | uomId: item.uomId | ||||
| } as SavePickOrderLineRequest)) | } as SavePickOrderLineRequest)) | ||||
| }; | }; | ||||
| console.log(`Submitting pick order for group ${groupName}:`, pickOrderData); | console.log(`Submitting pick order for group ${groupName}:`, pickOrderData); | ||||
| const res = await createPickOrder(pickOrderData); | const res = await createPickOrder(pickOrderData); | ||||
| @@ -835,7 +932,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| </TableCell> | </TableCell> | ||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} | |||||
| {item.targetDate&& item.targetDate !== "" ? new Date(item.targetDate).toLocaleDateString() : "-"} | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| </TableRow> | </TableRow> | ||||
| @@ -1001,12 +1098,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| ) | ) | ||||
| ); | ); | ||||
| // Auto-update created items if this item exists there | |||||
| setCreatedItems(prev => | |||||
| prev.map(item => | |||||
| item.itemId === itemId ? { ...item, qty: newQty || 1 } : item | |||||
| ) | |||||
| ); | |||||
| // Don't auto-add here - only on blur event | |||||
| }, []); | }, []); | ||||
| // Add checkbox change handler for second search | // Add checkbox change handler for second search | ||||
| @@ -1020,7 +1112,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| // 全选:将所有搜索结果添加到创建项目 | // 全选:将所有搜索结果添加到创建项目 | ||||
| secondSearchResults.forEach(item => { | secondSearchResults.forEach(item => { | ||||
| if (!isItemInCreated(item.id)) { | if (!isItemInCreated(item.id)) { | ||||
| handleSecondSearchItemSelect(item.id, true); | |||||
| handleSearchItemSelect(item.id, true); | |||||
| } | } | ||||
| }); | }); | ||||
| } else { | } else { | ||||
| @@ -1030,7 +1122,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| const isCurrentlyInCreated = isItemInCreated(item.id); | const isCurrentlyInCreated = isItemInCreated(item.id); | ||||
| if (isSelected && !isCurrentlyInCreated) { | if (isSelected && !isCurrentlyInCreated) { | ||||
| handleSecondSearchItemSelect(item.id, true); | |||||
| handleSearchItemSelect(item.id, true); | |||||
| } else if (!isSelected && isCurrentlyInCreated) { | } else if (!isSelected && isCurrentlyInCreated) { | ||||
| setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id)); | setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id)); | ||||
| } | } | ||||
| @@ -1045,7 +1137,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| newlySelected.forEach(id => { | newlySelected.forEach(id => { | ||||
| if (!isItemInCreated(id as number)) { | if (!isItemInCreated(id as number)) { | ||||
| handleSecondSearchItemSelect(id as number, true); | |||||
| handleSearchItemSelect(id as number, true); | |||||
| } | } | ||||
| }); | }); | ||||
| @@ -1053,7 +1145,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id)); | setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id)); | ||||
| }); | }); | ||||
| } | } | ||||
| }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSecondSearchItemSelect]); | |||||
| }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSearchItemSelect]); | |||||
| // Update the secondSearchItemColumns to add right alignment for Current Stock and Order Quantity | // Update the secondSearchItemColumns to add right alignment for Current Stock and Order Quantity | ||||
| const secondSearchItemColumns: Column<SearchItemWithQty>[] = useMemo(() => [ | const secondSearchItemColumns: Column<SearchItemWithQty>[] = useMemo(() => [ | ||||
| @@ -1211,7 +1303,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| setIsLoadingSecondSearch(false); | setIsLoadingSecondSearch(false); | ||||
| }, 500); | }, 500); | ||||
| }, [items, formProps]); | }, [items, formProps]); | ||||
| /* | |||||
| // Create a custom search box component that displays fields vertically | // Create a custom search box component that displays fields vertically | ||||
| const VerticalSearchBox = ({ criteria, onSearch, onReset }: { | const VerticalSearchBox = ({ criteria, onSearch, onReset }: { | ||||
| criteria: Criterion<any>[]; | criteria: Criterion<any>[]; | ||||
| @@ -1255,6 +1347,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| options={c.options || []} | options={c.options || []} | ||||
| getOptionLabel={(option: any) => option.label} | getOptionLabel={(option: any) => option.label} | ||||
| onChange={(_, value: any) => handleInputChange(c.paramName, value?.value || "")} | onChange={(_, value: any) => handleInputChange(c.paramName, value?.value || "")} | ||||
| value={c.options?.find(option => option.value === inputs[c.paramName]) || null} | |||||
| renderInput={(params) => ( | renderInput={(params) => ( | ||||
| <TextField | <TextField | ||||
| {...params} | {...params} | ||||
| @@ -1288,7 +1381,7 @@ const NewCreateItem: React.FC<Props> = ({ filterArgs, searchQuery, onPickOrderCr | |||||
| </Card> | </Card> | ||||
| ); | ); | ||||
| }; | }; | ||||
| */ | |||||
| // Add pagination state for search results | // Add pagination state for search results | ||||
| const [searchResultsPagingController, setSearchResultsPagingController] = useState({ | const [searchResultsPagingController, setSearchResultsPagingController] = useState({ | ||||
| pageNum: 1, | pageNum: 1, | ||||
| @@ -1386,39 +1479,103 @@ const getValidationMessage = useCallback(() => { | |||||
| }, [secondSearchResults, selectedSecondSearchItemIds]); | }, [secondSearchResults, selectedSecondSearchItemIds]); | ||||
| // Move these handlers to the component level (outside of CustomSearchResultsTable) | // Move these handlers to the component level (outside of CustomSearchResultsTable) | ||||
| // Handle individual checkbox change - ONLY select, don't add to created items | |||||
| const handleIndividualCheckboxChange = useCallback((itemId: number, checked: boolean) => { | |||||
| if (checked) { | |||||
| // Just add to selected IDs, don't auto-add to created items | |||||
| setSelectedSecondSearchItemIds(prev => [...prev, itemId]); | |||||
| // Handle individual checkbox change - ONLY select, don't add to created items | |||||
| const handleIndividualCheckboxChange = useCallback((itemId: number, checked: boolean) => { | |||||
| checkboxChangeCallCount++; | |||||
| // Set the item's group and targetDate to current group when selected | |||||
| setSecondSearchResults(prev => prev.map(item => | |||||
| item.id === itemId | |||||
| ? { | |||||
| ...item, | |||||
| groupId: selectedGroup?.id || undefined, | |||||
| targetDate: selectedGroup?.targetDate || undefined | |||||
| if (checked) { | |||||
| // Add to selected IDs | |||||
| setSelectedSecondSearchItemIds(prev => [...prev, itemId]); | |||||
| // Set the item's group and targetDate to current group when selected | |||||
| setSecondSearchResults(prev => { | |||||
| const updatedResults = prev.map(item => | |||||
| item.id === itemId | |||||
| ? { | |||||
| ...item, | |||||
| groupId: selectedGroup?.id || undefined, | |||||
| targetDate: selectedGroup?.targetDate !== undefined && selectedGroup?.targetDate !== "" ? selectedGroup.targetDate : undefined | |||||
| } | |||||
| : item | |||||
| ); | |||||
| // Check if should auto-add after state update | |||||
| setTimeout(() => { | |||||
| // Check if we're already processing this item | |||||
| if (processingItems.has(itemId)) { | |||||
| //alert(`Item ${itemId} is already being processed, skipping duplicate auto-add`); | |||||
| return; | |||||
| } | } | ||||
| : item | |||||
| )); | |||||
| } else { | |||||
| // Just remove from selected IDs, don't remove from created items | |||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); | |||||
| // Clear the item's group and targetDate when deselected | |||||
| setSecondSearchResults(prev => prev.map(item => | |||||
| item.id === itemId | |||||
| ? { | |||||
| ...item, | |||||
| groupId: undefined, | |||||
| targetDate: undefined | |||||
| const updatedItem = updatedResults.find(i => i.id === itemId); | |||||
| if (updatedItem) { | |||||
| const isSelected = true; // We just selected it | |||||
| const hasGroup = updatedItem.groupId !== undefined && updatedItem.groupId !== null; | |||||
| const hasQty = updatedItem.qty !== null && updatedItem.qty !== undefined && updatedItem.qty > 0; | |||||
| // Only auto-add if item has quantity (scenario 2: enter quantity first, then select) | |||||
| if (isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)) { | |||||
| // Mark this item as being processed | |||||
| processingItems.add(itemId); | |||||
| const newCreatedItem: CreatedItem = { | |||||
| itemId: updatedItem.id, | |||||
| itemName: updatedItem.label, | |||||
| itemCode: updatedItem.label, | |||||
| qty: updatedItem.qty || 1, | |||||
| uom: updatedItem.uom || "", | |||||
| uomId: updatedItem.uomId || 0, | |||||
| uomDesc: updatedItem.uomDesc || "", | |||||
| isSelected: true, | |||||
| currentStockBalance: updatedItem.currentStockBalance, | |||||
| targetDate: updatedItem.targetDate || targetDate, | |||||
| groupId: updatedItem.groupId || undefined, | |||||
| }; | |||||
| setCreatedItems(prev => [...prev, newCreatedItem]); | |||||
| setSecondSearchResults(current => current.filter(searchItem => searchItem.id !== itemId)); | |||||
| setSelectedSecondSearchItemIds(current => current.filter(id => id !== itemId)); | |||||
| // Remove from processing set after a short delay | |||||
| setTimeout(() => { | |||||
| processingItems.delete(itemId); | |||||
| }, 100); | |||||
| } | |||||
| // Show final debug info in one alert | |||||
| /* | |||||
| alert(`FINAL DEBUG INFO for item ${itemId}: | |||||
| Function called ${checkboxChangeCallCount} times | |||||
| Is Selected: ${isSelected} | |||||
| Has Group: ${hasGroup} | |||||
| Has Quantity: ${hasQty} | |||||
| Quantity: ${updatedItem.qty} | |||||
| Group ID: ${updatedItem.groupId} | |||||
| Is Item In Created: ${isItemInCreated(updatedItem.id)} | |||||
| Auto-add triggered: ${isSelected && hasGroup && hasQty && !isItemInCreated(updatedItem.id)} | |||||
| Processing items: ${Array.from(processingItems).join(', ')}`); | |||||
| */ | |||||
| } | } | ||||
| : item | |||||
| )); | |||||
| } | |||||
| }, [selectedGroup]); | |||||
| }, 0); | |||||
| return updatedResults; | |||||
| }); | |||||
| } else { | |||||
| // Remove from selected IDs | |||||
| setSelectedSecondSearchItemIds(prev => prev.filter(id => id !== itemId)); | |||||
| // Clear the item's group and targetDate when deselected | |||||
| setSecondSearchResults(prev => prev.map(item => | |||||
| item.id === itemId | |||||
| ? { | |||||
| ...item, | |||||
| groupId: undefined, | |||||
| targetDate: undefined | |||||
| } | |||||
| : item | |||||
| )); | |||||
| } | |||||
| }, [selectedGroup, isItemInCreated, targetDate]); | |||||
| // Handle select all checkbox for current page | // Handle select all checkbox for current page | ||||
| const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: SearchItemWithQty[]) => { | const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: SearchItemWithQty[]) => { | ||||
| @@ -1439,7 +1596,7 @@ const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: S | |||||
| ? { | ? { | ||||
| ...item, | ...item, | ||||
| groupId: selectedGroup?.id || undefined, | groupId: selectedGroup?.id || undefined, | ||||
| targetDate: selectedGroup?.targetDate || undefined | |||||
| targetDate: selectedGroup?.targetDate !== undefined && selectedGroup.targetDate !== "" ? selectedGroup.targetDate : undefined | |||||
| } | } | ||||
| : item | : item | ||||
| )); | )); | ||||
| @@ -1462,6 +1619,7 @@ const handleSelectAllOnPage = useCallback((checked: boolean, paginatedResults: S | |||||
| }, [selectedGroup, isItemInCreated]); | }, [selectedGroup, isItemInCreated]); | ||||
| // Update the CustomSearchResultsTable to use the handlers from component level | // Update the CustomSearchResultsTable to use the handlers from component level | ||||
| /* | |||||
| const CustomSearchResultsTable = () => { | const CustomSearchResultsTable = () => { | ||||
| // Calculate pagination | // Calculate pagination | ||||
| const startIndex = (searchResultsPagingController.pageNum - 1) * searchResultsPagingController.pageSize; | const startIndex = (searchResultsPagingController.pageNum - 1) * searchResultsPagingController.pageSize; | ||||
| @@ -1524,7 +1682,7 @@ const CustomSearchResultsTable = () => { | |||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Item */} | |||||
| <TableCell> | <TableCell> | ||||
| <Box> | <Box> | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| @@ -1536,7 +1694,7 @@ const CustomSearchResultsTable = () => { | |||||
| </Box> | </Box> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Group - Show the item's own group (or "-" if not selected) */} | |||||
| <TableCell> | <TableCell> | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| {(() => { | {(() => { | ||||
| @@ -1549,7 +1707,7 @@ const CustomSearchResultsTable = () => { | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Current Stock */} | |||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Typography | <Typography | ||||
| variant="body2" | variant="body2" | ||||
| @@ -1560,14 +1718,13 @@ const CustomSearchResultsTable = () => { | |||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Stock Unit */} | |||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| {item.uomDesc || "-"} | {item.uomDesc || "-"} | ||||
| </Typography> | </Typography> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Order Quantity */} | |||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <TextField | <TextField | ||||
| type="number" | type="number" | ||||
| @@ -1581,6 +1738,10 @@ const CustomSearchResultsTable = () => { | |||||
| handleSecondSearchQtyChange(item.id, numValue); | handleSecondSearchQtyChange(item.id, numValue); | ||||
| } | } | ||||
| }} | }} | ||||
| onBlur={() => { | |||||
| // Trigger auto-add check when user finishes input | |||||
| handleQtyBlur(item.id); | |||||
| }} | |||||
| inputProps={{ | inputProps={{ | ||||
| style: { textAlign: 'center' } | style: { textAlign: 'center' } | ||||
| }} | }} | ||||
| @@ -1594,7 +1755,7 @@ const CustomSearchResultsTable = () => { | |||||
| /> | /> | ||||
| </TableCell> | </TableCell> | ||||
| {/* Target Date - Show the item's own target date (or "-" if not selected) */} | |||||
| <TableCell align="right"> | <TableCell align="right"> | ||||
| <Typography variant="body2"> | <Typography variant="body2"> | ||||
| {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} | {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} | ||||
| @@ -1607,7 +1768,7 @@ const CustomSearchResultsTable = () => { | |||||
| </Table> | </Table> | ||||
| </TableContainer> | </TableContainer> | ||||
| {/* Add pagination for search results */} | |||||
| <TablePagination | <TablePagination | ||||
| component="div" | component="div" | ||||
| count={secondSearchResults.length} | count={secondSearchResults.length} | ||||
| @@ -1624,6 +1785,7 @@ const CustomSearchResultsTable = () => { | |||||
| </> | </> | ||||
| ); | ); | ||||
| }; | }; | ||||
| */ | |||||
| // Add helper function to get group range text | // Add helper function to get group range text | ||||
| const getGroupRangeText = useCallback(() => { | const getGroupRangeText = useCallback(() => { | ||||
| @@ -1694,10 +1856,11 @@ const CustomSearchResultsTable = () => { | |||||
| <Grid item> | <Grid item> | ||||
| <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk"> | <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="zh-hk"> | ||||
| <DatePicker | <DatePicker | ||||
| value={dayjs(selectedGroup.targetDate)} | |||||
| value={selectedGroup.targetDate && selectedGroup.targetDate !== "" ? dayjs(selectedGroup.targetDate) : null} | |||||
| onChange={(date) => { | onChange={(date) => { | ||||
| if (date) { | if (date) { | ||||
| const formattedDate = date.format(INPUT_DATE_FORMAT); | const formattedDate = date.format(INPUT_DATE_FORMAT); | ||||
| handleGroupTargetDateChange(selectedGroup.id, formattedDate); | handleGroupTargetDateChange(selectedGroup.id, formattedDate); | ||||
| } | } | ||||
| }} | }} | ||||
| @@ -1728,29 +1891,41 @@ const CustomSearchResultsTable = () => { | |||||
| {/* Second Search Results - Use custom table like AssignAndRelease */} | {/* Second Search Results - Use custom table like AssignAndRelease */} | ||||
| {hasSearchedSecond && ( | {hasSearchedSecond && ( | ||||
| <Box sx={{ mt: 3 }}> | |||||
| <Typography variant="h6" marginBlockEnd={2}> | |||||
| {t("Search Results")} ({secondSearchResults.length}) | |||||
| </Typography> | |||||
| {/* Add selected items info text */} | |||||
| {selectedSecondSearchItemIds.length > 0 && ( | |||||
| <Box sx={{ mb: 2 }}> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}> | |||||
| {t("Selected items will join above created group")} | |||||
| </Typography> | |||||
| </Box> | |||||
| )} | |||||
| {isLoadingSecondSearch ? ( | |||||
| <Typography>{t("Loading...")}</Typography> | |||||
| ) : secondSearchResults.length === 0 ? ( | |||||
| <Typography color="textSecondary">{t("No results found")}</Typography> | |||||
| ) : ( | |||||
| <CustomSearchResultsTable /> | |||||
| )} | |||||
| </Box> | |||||
| )} | |||||
| <Box sx={{ mt: 3 }}> | |||||
| <Typography variant="h6" marginBlockEnd={2}> | |||||
| {t("Search Results")} ({secondSearchResults.length}) | |||||
| </Typography> | |||||
| {selectedSecondSearchItemIds.length > 0 && ( | |||||
| <Box sx={{ mb: 2 }}> | |||||
| <Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}> | |||||
| {t("Selected items will join above created group")} | |||||
| </Typography> | |||||
| </Box> | |||||
| )} | |||||
| {isLoadingSecondSearch ? ( | |||||
| <Typography>{t("Loading...")}</Typography> | |||||
| ) : secondSearchResults.length === 0 ? ( | |||||
| <Typography color="textSecondary">{t("No results found")}</Typography> | |||||
| ) : ( | |||||
| <SearchResultsTable | |||||
| items={secondSearchResults} | |||||
| selectedItemIds={selectedSecondSearchItemIds} | |||||
| groups={groups} | |||||
| onItemSelect={handleIndividualCheckboxChange} | |||||
| onQtyChange={handleSecondSearchQtyChange} | |||||
| onGroupChange={handleCreatedItemGroupChange} | |||||
| onQtyBlur={handleQtyBlur} | |||||
| isItemInCreated={isItemInCreated} | |||||
| pageNum={searchResultsPagingController.pageNum} | |||||
| pageSize={searchResultsPagingController.pageSize} | |||||
| onPageChange={handleSearchResultsPageChange} | |||||
| onPageSizeChange={handleSearchResultsPageSizeChange} | |||||
| /> | |||||
| )} | |||||
| </Box> | |||||
| )} | |||||
| {/* Add Submit Button between tables */} | {/* Add Submit Button between tables */} | ||||
| @@ -1784,14 +1959,24 @@ const CustomSearchResultsTable = () => { | |||||
| {/* 创建项目区域 - 修改Group列为可选择的 */} | {/* 创建项目区域 - 修改Group列为可选择的 */} | ||||
| {createdItems.length > 0 && ( | {createdItems.length > 0 && ( | ||||
| <Box sx={{ mt: 3 }}> | |||||
| <Typography variant="h6" marginBlockEnd={2}> | |||||
| {t("Created Items")} ({createdItems.length}) | |||||
| </Typography> | |||||
| <CustomCreatedItemsTable /> | |||||
| </Box> | |||||
| )} | |||||
| <Box sx={{ mt: 3 }}> | |||||
| <Typography variant="h6" marginBlockEnd={2}> | |||||
| {t("Created Items")} ({createdItems.length}) | |||||
| </Typography> | |||||
| <CreatedItemsTable | |||||
| items={createdItems} | |||||
| groups={groups} | |||||
| onItemSelect={handleCreatedItemSelect} | |||||
| onQtyChange={handleQtyChange} | |||||
| onGroupChange={handleCreatedItemGroupChange} | |||||
| pageNum={createdItemsPagingController.pageNum} | |||||
| pageSize={createdItemsPagingController.pageSize} | |||||
| onPageChange={handleCreatedItemsPageChange} | |||||
| onPageSizeChange={handleCreatedItemsPageSizeChange} | |||||
| /> | |||||
| </Box> | |||||
| )} | |||||
| {/* 操作按钮 */} | {/* 操作按钮 */} | ||||
| <Stack direction="row" justifyContent="flex-start" gap={1} sx={{ mt: 3 }}> | <Stack direction="row" justifyContent="flex-start" gap={1} sx={{ mt: 3 }}> | ||||
| @@ -25,15 +25,15 @@ | |||||
| "Bind Storage": "綁定倉位", | "Bind Storage": "綁定倉位", | ||||
| "itemNo": "貨品編號", | "itemNo": "貨品編號", | ||||
| "itemName": "貨品名稱", | "itemName": "貨品名稱", | ||||
| "qty": "訂單數量", | |||||
| "Require Qty": "需求數量", | |||||
| "qty": "訂單數", | |||||
| "Require Qty": "需求數", | |||||
| "uom": "計量單位", | "uom": "計量單位", | ||||
| "total weight": "總重量", | "total weight": "總重量", | ||||
| "weight unit": "重量單位", | "weight unit": "重量單位", | ||||
| "price": "訂單貨值", | "price": "訂單貨值", | ||||
| "processed": "已入倉", | "processed": "已入倉", | ||||
| "expiryDate": "到期日", | "expiryDate": "到期日", | ||||
| "acceptedQty": "是次訂單/來貨/巳來貨數量", | |||||
| "acceptedQty": "是次訂單/來貨/巳來貨數", | |||||
| "weight": "重量", | "weight": "重量", | ||||
| "start": "開始", | "start": "開始", | ||||
| "qc": "質量控制", | "qc": "質量控制", | ||||
| @@ -41,7 +41,7 @@ | |||||
| "stock in": "入庫", | "stock in": "入庫", | ||||
| "putaway": "上架", | "putaway": "上架", | ||||
| "delete": "刪除", | "delete": "刪除", | ||||
| "qty cannot be greater than remaining qty": "數量不能大於剩餘數量", | |||||
| "qty cannot be greater than remaining qty": "數量不能大於剩餘數", | |||||
| "Record pol": "記錄採購訂單", | "Record pol": "記錄採購訂單", | ||||
| "Add some entries!": "添加條目!", | "Add some entries!": "添加條目!", | ||||
| "draft": "草稿", | "draft": "草稿", | ||||
| @@ -59,9 +59,9 @@ | |||||
| "value must be a number": "值必須是數字", | "value must be a number": "值必須是數字", | ||||
| "qc Check": "質量控制檢查", | "qc Check": "質量控制檢查", | ||||
| "Please select QC": "請選擇質量控制", | "Please select QC": "請選擇質量控制", | ||||
| "failQty": "失敗數量", | |||||
| "failQty": "失敗數", | |||||
| "select qc": "選擇質量控制", | "select qc": "選擇質量控制", | ||||
| "enter a failQty": "請輸入失敗數量", | |||||
| "enter a failQty": "請輸入失敗數", | |||||
| "qty too big": "數量過大", | "qty too big": "數量過大", | ||||
| "sampleRate": "抽樣率", | "sampleRate": "抽樣率", | ||||
| "sampleWeight": "樣本重量", | "sampleWeight": "樣本重量", | ||||
| @@ -76,7 +76,7 @@ | |||||
| "acceptedWeight": "接受重量", | "acceptedWeight": "接受重量", | ||||
| "productionDate": "生產日期", | "productionDate": "生產日期", | ||||
| "reportQty": "上報數量", | |||||
| "reportQty": "上報數", | |||||
| "Default Warehouse": "預設倉庫", | "Default Warehouse": "預設倉庫", | ||||
| "Select warehouse": "選擇倉庫", | "Select warehouse": "選擇倉庫", | ||||
| @@ -136,9 +136,9 @@ | |||||
| "Second Search Items": "第二搜尋項目", | "Second Search Items": "第二搜尋項目", | ||||
| "Second Search": "第二搜尋", | "Second Search": "第二搜尋", | ||||
| "Item": "貨品", | "Item": "貨品", | ||||
| "Order Quantity": "貨品需求數量", | |||||
| "Order Quantity": "貨品需求數", | |||||
| "Current Stock": "現時可用庫存", | "Current Stock": "現時可用庫存", | ||||
| "Selected": "已選擇", | |||||
| "Selected": "已選", | |||||
| "Select Items": "選擇貨品", | "Select Items": "選擇貨品", | ||||
| "Assign": "分派提料單", | "Assign": "分派提料單", | ||||
| "Release": "放單", | "Release": "放單", | ||||
| @@ -150,25 +150,27 @@ | |||||
| "End Product": "成品", | "End Product": "成品", | ||||
| "Lot Expiry Date": "批號到期日", | "Lot Expiry Date": "批號到期日", | ||||
| "Lot Location": "批號位置", | "Lot Location": "批號位置", | ||||
| "Available Lot": "批號可用提料數量", | |||||
| "Lot Required Pick Qty": "批號所需提料數量", | |||||
| "Lot Actual Pick Qty": "批號實際提料數量", | |||||
| "Available Lot": "批號可用提料數", | |||||
| "Lot Required Pick Qty": "批號所需提料數", | |||||
| "Lot Actual Pick Qty": "批號實際提料數", | |||||
| "Lot#": "批號", | "Lot#": "批號", | ||||
| "Submit": "提交", | "Submit": "提交", | ||||
| "Created Items": "已建立貨品", | "Created Items": "已建立貨品", | ||||
| "Create New Group": "建立新分組", | |||||
| "Create New Group": "建立新提料分組", | |||||
| "Group": "分組", | "Group": "分組", | ||||
| "Qty Already Picked": "已提料數量", | |||||
| "Qty Already Picked": "已提料數", | |||||
| "Select Job Order Items": "選擇工單貨品", | "Select Job Order Items": "選擇工單貨品", | ||||
| "failedQty": "不合格項目數量", | |||||
| "failedQty": "不合格項目數", | |||||
| "remarks": "備註", | "remarks": "備註", | ||||
| "Qc items": "QC 項目", | "Qc items": "QC 項目", | ||||
| "qcItem": "QC 項目", | "qcItem": "QC 項目", | ||||
| "QC Info": "QC 資訊", | "QC Info": "QC 資訊", | ||||
| "qcResult": "QC 結果", | "qcResult": "QC 結果", | ||||
| "acceptQty": "接受數量", | |||||
| "acceptQty": "接受數", | |||||
| "Escalation History": "上報歷史", | "Escalation History": "上報歷史", | ||||
| "Group Name": "分組名稱", | |||||
| "Job Order Code": "工單編號" | |||||
| "Group Code": "分組編號", | |||||
| "Job Order Code": "工單編號", | |||||
| "QC Check": "QC 檢查", | |||||
| "QR Code Scan": "QR Code掃描" | |||||
| } | } | ||||