| @@ -119,6 +119,98 @@ export interface CurrentInventoryItemInfo { | |||
| requiredQty: number; | |||
| } | |||
| export interface SavePickOrderGroupRequest { | |||
| groupIds?: number[]; | |||
| names?: string[]; | |||
| targetDate?: string; | |||
| pickOrderId?: number | null; | |||
| } | |||
| export interface PickOrderGroupInfo { | |||
| id: number; | |||
| name: string; | |||
| targetDate: string | null; | |||
| pickOrderId: number | null; | |||
| } | |||
| export interface AssignPickOrderInputs { | |||
| pickOrderIds: number[]; | |||
| assignTo: number; | |||
| } | |||
| // Missing function 1: newassignPickOrder | |||
| export const newassignPickOrder = async (data: AssignPickOrderInputs) => { | |||
| const response = await serverFetchJson<PostPickOrderResponse>( | |||
| `${BASE_API_URL}/pickOrder/assign`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| revalidateTag("pickorder"); | |||
| return response; | |||
| }; | |||
| // Missing function 2: releaseAssignedPickOrders | |||
| export const releaseAssignedPickOrders = async (data: AssignPickOrderInputs) => { | |||
| const response = await serverFetchJson<PostPickOrderResponse>( | |||
| `${BASE_API_URL}/pickOrder/release-assigned`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| revalidateTag("pickorder"); | |||
| return response; | |||
| }; | |||
| // Get latest group name and create it automatically | |||
| export const getLatestGroupNameAndCreate = async () => { | |||
| return serverFetchJson<PostPickOrderResponse>( | |||
| `${BASE_API_URL}/pickOrder/groups/latest`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["pickorder"] }, | |||
| }, | |||
| ); | |||
| }; | |||
| // Get all groups | |||
| export const fetchAllGroups = cache(async () => { | |||
| return serverFetchJson<PickOrderGroupInfo[]>( | |||
| `${BASE_API_URL}/pickOrder/groups/list`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["pickorder"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| // Create or update groups (flexible - can handle both cases) | |||
| export const createOrUpdateGroups = async (data: SavePickOrderGroupRequest) => { | |||
| const response = await serverFetchJson<PostPickOrderResponse>( | |||
| `${BASE_API_URL}/pickOrder/groups/create`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| revalidateTag("pickorder"); | |||
| return response; | |||
| }; | |||
| // Get groups by pick order ID | |||
| export const fetchGroupsByPickOrderId = cache(async (pickOrderId: number) => { | |||
| return serverFetchJson<PickOrderGroupInfo[]>( | |||
| `${BASE_API_URL}/pickOrder/groups/${pickOrderId}`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["pickorder"] }, | |||
| }, | |||
| ); | |||
| }); | |||
| export const fetchPickOrderDetails = cache(async (ids: string) => { | |||
| return serverFetchJson<GetPickOrderInfoResponse>( | |||
| @@ -16,7 +16,25 @@ export interface QcResult { | |||
| stockOutLineId?: number; | |||
| failQty: number; | |||
| } | |||
| export interface SaveQcResultRequest { | |||
| qcItemId: number; | |||
| itemId: number; | |||
| stockInLineId: number | null; | |||
| stockOutLineId: number; | |||
| failQty: number; | |||
| type: string; | |||
| remarks: string; | |||
| qcPassed: boolean; | |||
| } | |||
| export interface SaveQcResultResponse { | |||
| id: number | null; | |||
| name: string; | |||
| code: string; | |||
| type?: string; | |||
| message: string | null; | |||
| errorPosition: string; | |||
| } | |||
| export const fetchQcItemCheck = cache(async (itemId?: number) => { | |||
| let url = `${BASE_API_URL}/qcCheck`; | |||
| if (itemId) url += `/${itemId}`; | |||
| @@ -39,3 +57,15 @@ export const fetchPickOrderQcResult = cache(async (id: number) => { | |||
| }, | |||
| ); | |||
| }); | |||
| export const savePickOrderQcResult = async (data: SaveQcResultRequest) => { | |||
| const response = await serverFetchJson<SaveQcResultResponse>( | |||
| `${BASE_API_URL}/qcResult/new`, | |||
| { | |||
| method: "POST", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| revalidateTag("qc"); | |||
| return response; | |||
| }; | |||
| @@ -6,11 +6,12 @@ import { | |||
| } from "@/app/utils/fetchUtil"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { CreateItemResponse } from "../../utils"; | |||
| import { CreateItemResponse, RecordsRes } from "../../utils"; | |||
| import { ItemQc, ItemsResult } from "."; | |||
| import { QcChecksInputs } from "../qcCheck/actions"; | |||
| import { cache } from "react"; | |||
| // export type TypeInputs = { | |||
| // id: number; | |||
| // name: string | |||
| @@ -56,6 +57,7 @@ export interface ItemCombo { | |||
| label: string, | |||
| uomId: number, | |||
| uom: string, | |||
| uomDesc: string, | |||
| group?: string, | |||
| currentStockBalance?: number, | |||
| } | |||
| @@ -65,3 +67,25 @@ export const fetchAllItemsInClient = cache(async () => { | |||
| next: { tags: ["items"] }, | |||
| }); | |||
| }); | |||
| export const fetchPickOrderItemsByPageClient = cache( | |||
| async (queryParams?: Record<string, any>) => { | |||
| if (queryParams) { | |||
| const queryString = new URLSearchParams(queryParams).toString(); | |||
| return serverFetchJson<RecordsRes<any>>( | |||
| `${BASE_API_URL}/items/pickOrderItems?${queryString}`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["pickorder"] }, | |||
| }, | |||
| ); | |||
| } else { | |||
| return serverFetchJson<RecordsRes<any>>( | |||
| `${BASE_API_URL}/items/pickOrderItems`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["pickorder"] }, | |||
| }, | |||
| ); | |||
| } | |||
| }, | |||
| ); | |||
| @@ -30,6 +30,13 @@ export interface NameList { | |||
| name: string; | |||
| } | |||
| export interface NewNameList { | |||
| id: number; | |||
| name: string; | |||
| title: string; | |||
| department: string; | |||
| } | |||
| export const fetchUserDetails = cache(async (id: number) => { | |||
| return serverFetchJson<UserDetail>(`${BASE_API_URL}/user/${id}`, { | |||
| next: { tags: ["user"] }, | |||
| @@ -42,6 +49,12 @@ export const fetchNameList = cache(async () => { | |||
| }); | |||
| }); | |||
| export const fetchNewNameList = cache(async () => { | |||
| return serverFetchJson<NewNameList[]>(`${BASE_API_URL}/user/new-name-list`, { | |||
| next: { tags: ["user"] }, | |||
| }); | |||
| }); | |||
| export const editUser = async (id: number, data: UserInputs) => { | |||
| const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { | |||
| method: "PUT", | |||
| @@ -9,9 +9,6 @@ import { | |||
| Modal, | |||
| TextField, | |||
| Typography, | |||
| Accordion, | |||
| AccordionSummary, | |||
| AccordionDetails, | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| @@ -19,36 +16,27 @@ import { | |||
| TableHead, | |||
| TableRow, | |||
| Paper, | |||
| Checkbox, | |||
| TablePagination, | |||
| Alert, | |||
| AlertTitle, | |||
| } from "@mui/material"; | |||
| import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import SearchResults, { Column } from "../SearchResults/SearchResults"; | |||
| import { | |||
| PickOrderResult, | |||
| } from "@/app/api/pickOrder"; | |||
| import { | |||
| assignPickOrder, | |||
| fetchPickOrderClient, | |||
| newassignPickOrder, | |||
| AssignPickOrderInputs, | |||
| fetchPickOrderWithStockClient, | |||
| releasePickOrder, | |||
| ReleasePickOrderInputs, | |||
| GetPickOrderInfo, | |||
| GetPickOrderLineInfo, | |||
| } from "@/app/api/pickOrder/actions"; | |||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||
| import { | |||
| FormProvider, | |||
| useForm, | |||
| } from "react-hook-form"; | |||
| import { isEmpty, upperCase, upperFirst } from "lodash"; | |||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { fetchNameList, NameList ,fetchNewNameList, NewNameList} from "@/app/api/user/actions"; | |||
| import { FormProvider, useForm } from "react-hook-form"; | |||
| import { isEmpty, sortBy, uniqBy, upperFirst, groupBy } from "lodash"; | |||
| import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil"; | |||
| import useUploadContext from "../UploadProvider/useUploadContext"; | |||
| import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| import { flatten, intersectionWith, sortBy, uniqBy } from "lodash"; | |||
| import { arrayToDayjs } from "@/app/utils/formatUtil"; | |||
| import { fetchPickOrderItemsByPageClient } from "@/app/api/settings/item/actions"; | |||
| dayjs.extend(arraySupport); | |||
| @@ -56,6 +44,56 @@ interface Props { | |||
| filterArgs: Record<string, any>; | |||
| } | |||
| // 使用 fetchPickOrderItemsByPageClient 返回的数据结构 | |||
| interface ItemRow { | |||
| id: string; | |||
| pickOrderId: number; | |||
| pickOrderCode: string; | |||
| itemId: number; | |||
| itemCode: string; | |||
| itemName: string; | |||
| requiredQty: number; | |||
| currentStock: number; | |||
| unit: string; | |||
| targetDate: any; | |||
| status: string; | |||
| consoCode?: string; | |||
| assignTo?: number; | |||
| groupName?: string; | |||
| } | |||
| // 分组后的数据结构 | |||
| interface GroupedItemRow { | |||
| pickOrderId: number; | |||
| pickOrderCode: string; | |||
| targetDate: any; | |||
| status: string; | |||
| consoCode?: string; | |||
| items: ItemRow[]; | |||
| } | |||
| // 新增的 PickOrderRow 和 PickOrderLineRow 接口 | |||
| interface PickOrderRow { | |||
| id: string; // Change from number to string to match API response | |||
| code: string; | |||
| targetDate: string; | |||
| type: string; | |||
| status: string; | |||
| assignTo: number; | |||
| groupName: string; | |||
| consoCode?: string; | |||
| pickOrderLines: PickOrderLineRow[]; | |||
| } | |||
| interface PickOrderLineRow { | |||
| id: string; | |||
| itemCode: string; | |||
| itemName: string; | |||
| requiredQty: number; | |||
| availableQty: number; | |||
| uomDesc: string; | |||
| } | |||
| const style = { | |||
| position: "absolute", | |||
| top: "50%", | |||
| @@ -71,73 +109,85 @@ const style = { | |||
| const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| const { setIsUploading } = useUploadContext(); | |||
| // State for Pick Orders | |||
| const [selectedRows, setSelectedRows] = useState<(string | number)[]>([]); | |||
| const [filteredPickOrder, setFilteredPickOrder] = useState([] as GetPickOrderInfo[]); | |||
| const [isLoadingPickOrders, setIsLoadingPickOrders] = useState(false); | |||
| // Update state to use pick order data directly | |||
| const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<string[]>([]); // Change from number[] to string[] | |||
| const [filteredPickOrders, setFilteredPickOrders] = useState<PickOrderRow[]>([]); | |||
| const [isLoadingItems, setIsLoadingItems] = useState(false); | |||
| const [pagingController, setPagingController] = useState({ | |||
| pageNum: 0, | |||
| pageNum: 1, | |||
| pageSize: 10, | |||
| }); | |||
| const [totalCountPickOrders, setTotalCountPickOrders] = useState<number>(); | |||
| // State for Assign & Release Modal | |||
| const [totalCountItems, setTotalCountItems] = useState<number>(); | |||
| const [modalOpen, setModalOpen] = useState(false); | |||
| const [usernameList, setUsernameList] = useState<NameList[]>([]); | |||
| // Add search state | |||
| const [usernameList, setUsernameList] = useState<NewNameList[]>([]); | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||
| const [originalPickOrderData, setOriginalPickOrderData] = useState([] as GetPickOrderInfo[]); | |||
| const [originalPickOrderData, setOriginalPickOrderData] = useState<PickOrderRow[]>([]); | |||
| const formProps = useForm<ReleasePickOrderInputs>(); | |||
| const formProps = useForm<AssignPickOrderInputs>(); | |||
| const errors = formProps.formState.errors; | |||
| // Fetch Pick Orders with Stock Information | |||
| const fetchNewPagePickOrder = useCallback( | |||
| async ( | |||
| pagingController: Record<string, number>, | |||
| filterArgs: Record<string, number>, | |||
| ) => { | |||
| setIsLoadingPickOrders(true); | |||
| const params = { | |||
| ...pagingController, | |||
| ...filterArgs, | |||
| }; | |||
| const res = await fetchPickOrderWithStockClient(params); | |||
| if (res) { | |||
| console.log(res); | |||
| setFilteredPickOrder(res.records); | |||
| setOriginalPickOrderData(res.records); // Store original data | |||
| setTotalCountPickOrders(res.total); | |||
| // Update the fetch function to process pick order data correctly | |||
| const fetchNewPageItems = useCallback( | |||
| async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => { | |||
| setIsLoadingItems(true); | |||
| try { | |||
| const params = { | |||
| ...pagingController, | |||
| ...filterArgs, | |||
| pageNum: (pagingController.pageNum || 1) - 1, | |||
| pageSize: pagingController.pageSize || 10, | |||
| }; | |||
| const res = await fetchPickOrderWithStockClient(params); | |||
| if (res && res.records) { | |||
| // Filter out assigned status if needed | |||
| const filteredRecords = res.records.filter((pickOrder: any) => pickOrder.status !== "assigned"); | |||
| // Convert pick order data to the expected format | |||
| const pickOrderRows: PickOrderRow[] = filteredRecords.map((pickOrder: any) => ({ | |||
| id: pickOrder.id, | |||
| code: pickOrder.code, | |||
| targetDate: pickOrder.targetDate, | |||
| type: pickOrder.type, | |||
| status: pickOrder.status, | |||
| assignTo: pickOrder.assignTo, | |||
| groupName: pickOrder.groupName || "No Group", | |||
| consoCode: pickOrder.consoCode, | |||
| pickOrderLines: pickOrder.pickOrderLines || [] | |||
| })); | |||
| setOriginalPickOrderData(pickOrderRows); | |||
| setFilteredPickOrders(pickOrderRows); | |||
| setTotalCountItems(res.total); | |||
| } else { | |||
| setFilteredPickOrders([]); | |||
| setTotalCountItems(0); | |||
| } | |||
| } catch (error) { | |||
| console.error("Error fetching pick orders:", error); | |||
| setFilteredPickOrders([]); | |||
| setTotalCountItems(0); | |||
| } finally { | |||
| setIsLoadingItems(false); | |||
| } | |||
| setIsLoadingPickOrders(false); | |||
| }, | |||
| [], | |||
| ); | |||
| // Add search criteria | |||
| // Update search criteria to match the new data structure | |||
| const searchCriteria: Criterion<any>[] = useMemo( | |||
| () => [ | |||
| { | |||
| label: t("Pick Order Code"), | |||
| paramName: "code", | |||
| type: "text" | |||
| { | |||
| label: t("Pick Order Code"), | |||
| paramName: "code", | |||
| type: "text", | |||
| }, | |||
| { | |||
| label: t("Type"), | |||
| paramName: "type", | |||
| type: "autocomplete", | |||
| options: sortBy( | |||
| uniqBy( | |||
| originalPickOrderData.map((po) => ({ | |||
| value: po.type, | |||
| label: t(upperCase(po.type)), | |||
| })), | |||
| "value", | |||
| ), | |||
| "label", | |||
| ), | |||
| label: t("Group Name"), | |||
| paramName: "groupName", | |||
| type: "text", | |||
| }, | |||
| { | |||
| label: t("Target Date From"), | |||
| @@ -146,14 +196,14 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||
| type: "dateRange", | |||
| }, | |||
| { | |||
| label: t("Status"), | |||
| label: t("Pick Order Status"), | |||
| paramName: "status", | |||
| type: "autocomplete", | |||
| options: sortBy( | |||
| uniqBy( | |||
| originalPickOrderData.map((po) => ({ | |||
| value: po.status, | |||
| label: t(upperFirst(po.status)), | |||
| originalPickOrderData.map((pickOrder) => ({ | |||
| value: pickOrder.status, | |||
| label: t(upperFirst(pickOrder.status)), | |||
| })), | |||
| "value", | |||
| ), | |||
| @@ -164,103 +214,144 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||
| [originalPickOrderData, t], | |||
| ); | |||
| // Add search handler | |||
| // Update search function to work with pick order data | |||
| const handleSearch = useCallback((query: Record<string, any>) => { | |||
| console.log("AssignAndRelease search triggered with query:", query); | |||
| setSearchQuery({ ...query }); | |||
| // Apply search filters to the data | |||
| const filtered = originalPickOrderData.filter((po) => { | |||
| const poTargetDateStr = arrayToDayjs(po.targetDate); | |||
| const filtered = originalPickOrderData.filter((pickOrder) => { | |||
| const pickOrderTargetDateStr = arrayToDayjs(pickOrder.targetDate); | |||
| const codeMatch = !query.code || | |||
| po.code?.toLowerCase().includes((query.code || "").toLowerCase()); | |||
| pickOrder.code?.toLowerCase().includes((query.code || "").toLowerCase()); | |||
| const dateMatch = !query.targetDate || | |||
| poTargetDateStr.isSame(query.targetDate) || | |||
| poTargetDateStr.isAfter(query.targetDate); | |||
| const groupNameMatch = !query.groupName || | |||
| pickOrder.groupName?.toLowerCase().includes((query.groupName || "").toLowerCase()); | |||
| const dateToMatch = !query.targetDateTo || | |||
| poTargetDateStr.isSame(query.targetDateTo) || | |||
| poTargetDateStr.isBefore(query.targetDateTo); | |||
| // Date range search | |||
| let dateMatch = true; | |||
| if (query.targetDate || query.targetDateTo) { | |||
| try { | |||
| if (query.targetDate && !query.targetDateTo) { | |||
| const fromDate = dayjs(query.targetDate); | |||
| dateMatch = pickOrderTargetDateStr.isSame(fromDate, 'day') || | |||
| pickOrderTargetDateStr.isAfter(fromDate, 'day'); | |||
| } else if (!query.targetDate && query.targetDateTo) { | |||
| const toDate = dayjs(query.targetDateTo); | |||
| dateMatch = pickOrderTargetDateStr.isSame(toDate, 'day') || | |||
| pickOrderTargetDateStr.isBefore(toDate, 'day'); | |||
| } else if (query.targetDate && query.targetDateTo) { | |||
| const fromDate = dayjs(query.targetDate); | |||
| const toDate = dayjs(query.targetDateTo); | |||
| dateMatch = (pickOrderTargetDateStr.isSame(fromDate, 'day') || | |||
| pickOrderTargetDateStr.isAfter(fromDate, 'day')) && | |||
| (pickOrderTargetDateStr.isSame(toDate, 'day') || | |||
| pickOrderTargetDateStr.isBefore(toDate, 'day')); | |||
| } | |||
| } catch (error) { | |||
| console.error("Date parsing error:", error); | |||
| dateMatch = true; | |||
| } | |||
| } | |||
| const statusMatch = !query.status || | |||
| query.status.toLowerCase() === "all" || | |||
| po.status?.toLowerCase().includes((query.status || "").toLowerCase()); | |||
| const typeMatch = !query.type || | |||
| query.type.toLowerCase() === "all" || | |||
| po.type?.toLowerCase().includes((query.type || "").toLowerCase()); | |||
| pickOrder.status?.toLowerCase().includes((query.status || "").toLowerCase()); | |||
| return codeMatch && dateMatch && dateToMatch && statusMatch && typeMatch; | |||
| return codeMatch && groupNameMatch && dateMatch && statusMatch; | |||
| }); | |||
| setFilteredPickOrder(filtered); | |||
| setFilteredPickOrders(filtered); | |||
| }, [originalPickOrderData]); | |||
| // Add reset handler | |||
| const handleReset = useCallback(() => { | |||
| setSearchQuery({}); | |||
| // Reset to original data | |||
| setFilteredPickOrder(originalPickOrderData); | |||
| setFilteredPickOrders(originalPickOrderData); | |||
| setTimeout(() => { | |||
| setSearchQuery({}); | |||
| }, 0); | |||
| }, [originalPickOrderData]); | |||
| // Handle Assign & Release | |||
| const handleAssignAndRelease = useCallback(async (data: ReleasePickOrderInputs) => { | |||
| if (selectedRows.length === 0) return; | |||
| // Fix the pagination handlers | |||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||
| const newPagingController = { | |||
| ...pagingController, | |||
| pageNum: newPage + 1, | |||
| }; | |||
| setPagingController(newPagingController); | |||
| }, [pagingController]); | |||
| const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |||
| const newPageSize = parseInt(event.target.value, 10); | |||
| const newPagingController = { | |||
| pageNum: 1, | |||
| pageSize: newPageSize, | |||
| }; | |||
| setPagingController(newPagingController); | |||
| }, []); | |||
| // 修复:处理 pick order 选择 | |||
| const handlePickOrderSelect = useCallback((pickOrderId: string, checked: boolean) => { | |||
| if (checked) { | |||
| setSelectedPickOrderIds(prev => [...prev, pickOrderId]); | |||
| } else { | |||
| setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId)); | |||
| } | |||
| }, []); | |||
| // 修复:检查 pick order 是否被选中 | |||
| const isPickOrderSelected = useCallback((pickOrderId: string) => { | |||
| return selectedPickOrderIds.includes(pickOrderId); | |||
| }, [selectedPickOrderIds]); | |||
| const handleAssignAndRelease = useCallback(async (data: AssignPickOrderInputs) => { | |||
| if (selectedPickOrderIds.length === 0) return; | |||
| setIsUploading(true); | |||
| try { | |||
| // First, assign the pick orders | |||
| const assignRes = await assignPickOrder(selectedRows as number[]); | |||
| if (assignRes) { | |||
| // Convert string IDs to numbers for the API | |||
| const numericIds = selectedPickOrderIds.map(id => parseInt(id, 10)); | |||
| const assignRes = await newassignPickOrder({ | |||
| pickOrderIds: numericIds, | |||
| assignTo: data.assignTo, | |||
| }); | |||
| if (assignRes && assignRes.code === "SUCCESS") { | |||
| console.log("Assign successful:", assignRes); | |||
| // Get the assign code from the response | |||
| const consoCode = assignRes.consoCode || assignRes.code; | |||
| if (consoCode) { | |||
| // Then, release the assign pick order | |||
| const releaseData = { | |||
| consoCode: consoCode, | |||
| assignTo: data.assignTo | |||
| }; | |||
| const releaseRes = await releasePickOrder(releaseData); | |||
| if (releaseRes) { | |||
| console.log("Release successful:", releaseRes); | |||
| setModalOpen(false); | |||
| // Clear selected rows | |||
| setSelectedRows([]); | |||
| // Refresh the pick orders list | |||
| fetchNewPagePickOrder(pagingController, filterArgs); | |||
| } | |||
| } | |||
| setModalOpen(false); | |||
| setSelectedPickOrderIds([]); // Clear selection | |||
| fetchNewPageItems(pagingController, filterArgs); | |||
| } else { | |||
| console.error("Assign failed:", assignRes); | |||
| } | |||
| } catch (error) { | |||
| console.error("Error in assign and release:", error); | |||
| console.error("Error in assign:", error); | |||
| } finally { | |||
| setIsUploading(false); | |||
| } | |||
| }, [selectedRows, setIsUploading, fetchNewPagePickOrder, pagingController, filterArgs]); | |||
| }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); | |||
| // Open assign & release modal | |||
| const openAssignModal = useCallback(() => { | |||
| setModalOpen(true); | |||
| // Reset form | |||
| formProps.reset(); | |||
| }, [formProps]); | |||
| // Load data | |||
| // Component mount effect | |||
| useEffect(() => { | |||
| fetchNewPagePickOrder(pagingController, filterArgs); | |||
| }, [fetchNewPagePickOrder, pagingController, filterArgs]); | |||
| fetchNewPageItems(pagingController, filterArgs || {}); | |||
| }, []); | |||
| // Dependencies change effect | |||
| useEffect(() => { | |||
| if (pagingController && (filterArgs || {})) { | |||
| fetchNewPageItems(pagingController, filterArgs || {}); | |||
| } | |||
| }, [pagingController, filterArgs, fetchNewPageItems]); | |||
| // Load username list | |||
| useEffect(() => { | |||
| const loadUsernameList = async () => { | |||
| try { | |||
| const res = await fetchNameList(); | |||
| const res = await fetchNewNameList(); | |||
| if (res) { | |||
| setUsernameList(res); | |||
| } | |||
| @@ -271,143 +362,141 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||
| loadUsernameList(); | |||
| }, []); | |||
| // Pick Orders columns with detailed item information | |||
| const pickOrderColumns = useMemo<Column<GetPickOrderInfo>[]>( | |||
| () => [ | |||
| { | |||
| name: "id", | |||
| label: "", | |||
| type: "checkbox", | |||
| disabled: (params) => { | |||
| return !isEmpty(params.consoCode); | |||
| }, | |||
| }, | |||
| { | |||
| name: "code", | |||
| label: t("Pick Order Code"), | |||
| }, | |||
| { | |||
| name: "pickOrderLines", | |||
| label: t("Items"), | |||
| renderCell: (params) => { | |||
| if (!params.pickOrderLines || params.pickOrderLines.length === 0) return ""; | |||
| return ( | |||
| <Accordion sx={{ boxShadow: 'none', '&:before': { display: 'none' } }}> | |||
| <AccordionSummary | |||
| expandIcon={<ExpandMoreIcon />} | |||
| sx={{ minHeight: 'auto', padding: 0 }} | |||
| > | |||
| <Typography variant="body2"> | |||
| {params.pickOrderLines.length} items | |||
| </Typography> | |||
| </AccordionSummary> | |||
| <AccordionDetails sx={{ padding: 1 }}> | |||
| <TableContainer component={Paper} sx={{ maxHeight: 200 }}> | |||
| <Table size="small"> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Item Name")}</TableCell> | |||
| <TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Required Qty")}</TableCell> | |||
| <TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Available Qty")}</TableCell> | |||
| <TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}>{t("Unit")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {params.pickOrderLines.map((line: GetPickOrderLineInfo, index: number) => ( | |||
| <TableRow key={index}> | |||
| <TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}> | |||
| {line.itemName} | |||
| </TableCell> | |||
| <TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}> | |||
| {line.requiredQty} | |||
| </TableCell> | |||
| <TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}> | |||
| <Typography | |||
| variant="caption" | |||
| //color={line.availableQty && line.availableQty >= line.requiredQty ? 'success.main' : 'error.main'} | |||
| > | |||
| {line.availableQty ?? 0} | |||
| </Typography> | |||
| </TableCell> | |||
| <TableCell sx={{ fontSize: '0.75rem', padding: '4px' }}> | |||
| {line.uomDesc} | |||
| </TableCell> | |||
| </TableRow> | |||
| ))} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| </AccordionDetails> | |||
| </Accordion> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| name: "targetDate", | |||
| label: t("Target Date"), | |||
| renderCell: (params) => { | |||
| return ( | |||
| dayjs(params.targetDate) | |||
| .add(-1, "month") | |||
| .format(OUTPUT_DATE_FORMAT) | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| name: "status", | |||
| label: t("Status"), | |||
| renderCell: (params) => { | |||
| return upperFirst(params.status); | |||
| }, | |||
| }, | |||
| ], | |||
| [t], | |||
| ); | |||
| // Update the table component to work with pick order data directly | |||
| const CustomPickOrderTable = () => { | |||
| return ( | |||
| <> | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell>{t("Selected")}</TableCell> | |||
| <TableCell>{t("Pick Order Code")}</TableCell> | |||
| <TableCell>{t("Group Name")}</TableCell> | |||
| <TableCell>{t("Item Code")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell align="right">{t("Order Quantity")}</TableCell> | |||
| <TableCell align="right">{t("Current Stock")}</TableCell> | |||
| <TableCell align="right">{t("Stock Unit")}</TableCell> | |||
| <TableCell>{t("Target Date")}</TableCell> | |||
| <TableCell>{t("Pick Order Status")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {filteredPickOrders.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={10} align="center"> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("No data available")} | |||
| </Typography> | |||
| </TableCell> | |||
| </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 */} | |||
| <TableCell> | |||
| {index === 0 ? ( | |||
| <Checkbox | |||
| checked={isPickOrderSelected(pickOrder.id)} | |||
| onChange={(e) => handlePickOrderSelect(pickOrder.id, e.target.checked)} | |||
| disabled={!isEmpty(pickOrder.consoCode)} | |||
| /> | |||
| ) : null} | |||
| </TableCell> | |||
| {/* Pick Order Code - only show for first line */} | |||
| <TableCell> | |||
| {index === 0 ? pickOrder.code : null} | |||
| </TableCell> | |||
| {/* Group Name - only show for first line */} | |||
| <TableCell> | |||
| {index === 0 ? pickOrder.groupName : null} | |||
| </TableCell> | |||
| {/* Item Code */} | |||
| <TableCell>{line.itemCode}</TableCell> | |||
| {/* Item Name */} | |||
| <TableCell>{line.itemName}</TableCell> | |||
| {/* Order Quantity */} | |||
| <TableCell align="right">{line.requiredQty}</TableCell> | |||
| {/* Current Stock */} | |||
| <TableCell align="right"> | |||
| <Typography | |||
| variant="body2" | |||
| color={line.availableQty && line.availableQty > 0 ? "success.main" : "error.main"} | |||
| sx={{ fontWeight: line.availableQty && line.availableQty > 0 ? 'bold' : 'normal' }} | |||
| > | |||
| {(line.availableQty || 0).toLocaleString()} | |||
| </Typography> | |||
| </TableCell> | |||
| {/* Unit */} | |||
| <TableCell align="right">{line.uomDesc}</TableCell> | |||
| {/* Target Date - only show for first line */} | |||
| <TableCell> | |||
| {index === 0 ? ( | |||
| arrayToDayjs(pickOrder.targetDate) | |||
| .add(-1, "month") | |||
| .format(OUTPUT_DATE_FORMAT) | |||
| ) : null} | |||
| </TableCell> | |||
| {/* Pick Order Status - only show for first line */} | |||
| <TableCell> | |||
| {index === 0 ? upperFirst(pickOrder.status) : null} | |||
| </TableCell> | |||
| </TableRow> | |||
| )) | |||
| )) | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| <TablePagination | |||
| component="div" | |||
| count={totalCountItems || 0} | |||
| page={(pagingController.pageNum - 1)} | |||
| rowsPerPage={pagingController.pageSize} | |||
| onPageChange={handlePageChange} | |||
| onRowsPerPageChange={handlePageSizeChange} | |||
| rowsPerPageOptions={[10, 25, 50, 100]} | |||
| labelRowsPerPage={t("Rows per page")} | |||
| labelDisplayedRows={({ from, to, count }) => | |||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||
| } | |||
| /> | |||
| </> | |||
| ); | |||
| }; | |||
| return ( | |||
| <> | |||
| {/* Search Box */} | |||
| <SearchBox | |||
| criteria={searchCriteria} | |||
| onSearch={handleSearch} | |||
| onReset={handleReset} | |||
| /> | |||
| {/* Pick Orders View */} | |||
| <SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} /> | |||
| <Grid container rowGap={1}> | |||
| {/* Remove the button from here */} | |||
| <Grid item xs={12}> | |||
| {isLoadingPickOrders ? ( | |||
| {isLoadingItems ? ( | |||
| <CircularProgress size={40} /> | |||
| ) : ( | |||
| <SearchResults<GetPickOrderInfo> | |||
| items={filteredPickOrder} | |||
| columns={pickOrderColumns} | |||
| pagingController={pagingController} | |||
| setPagingController={setPagingController} | |||
| totalCount={totalCountPickOrders} | |||
| checkboxIds={selectedRows!} | |||
| setCheckboxIds={setSelectedRows} | |||
| /> | |||
| <CustomPickOrderTable /> | |||
| )} | |||
| </Grid> | |||
| {/* Add the button below the table */} | |||
| <Grid item xs={12}> | |||
| <Box sx={{ display: 'flex', justifyContent: 'flex-start', mt: 2 }}> | |||
| <Box sx={{ display: "flex", justifyContent: "flex-start", mt: 2 }}> | |||
| <Button | |||
| disabled={selectedRows.length < 1} | |||
| disabled={selectedPickOrderIds.length < 1} | |||
| variant="outlined" | |||
| onClick={openAssignModal} | |||
| > | |||
| {t("Assign & Release")} | |||
| {t("Assign")} | |||
| </Button> | |||
| </Box> | |||
| </Grid> | |||
| </Grid> | |||
| {/* Assign & Release Modal */} | |||
| {modalOpen ? ( | |||
| <Modal | |||
| open={modalOpen} | |||
| @@ -419,25 +508,37 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||
| <Grid container rowGap={2}> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" component="h2"> | |||
| {t("assign & Release Pick Orders")} | |||
| {t("Assign Pick Orders")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Typography variant="body1" color="text.secondary"> | |||
| {t("Selected Pick Orders")}: {selectedRows.length} | |||
| {t("Selected Pick Orders")}: {selectedPickOrderIds.length} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <FormProvider {...formProps}> | |||
| <FormProvider {...formProps}> | |||
| <form onSubmit={formProps.handleSubmit(handleAssignAndRelease)}> | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={12}> | |||
| <FormControl fullWidth> | |||
| <Autocomplete | |||
| options={usernameList} | |||
| getOptionLabel={(option) => option.name} | |||
| getOptionLabel={(option) => { | |||
| // 修改:显示更详细的用户信息 | |||
| const title = option.title ? ` (${option.title})` : ''; | |||
| const department = option.department ? ` - ${option.department}` : ''; | |||
| return `${option.name}${title}${department}`; | |||
| }} | |||
| renderOption={(props, option) => ( | |||
| <Box component="li" {...props}> | |||
| <Typography variant="body1"> | |||
| {option.name} | |||
| {option.title && ` (${option.title})`} | |||
| {option.department && ` - ${option.department}`} | |||
| </Typography> | |||
| </Box> | |||
| )} | |||
| onChange={(_, value) => { | |||
| formProps.setValue("assignTo", value?.id || 0); | |||
| }} | |||
| @@ -455,23 +556,16 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Typography variant="body2" color="warning.main"> | |||
| {t("This action will assign the selected pick orders and release them immediately.")} | |||
| {t("This action will assign the selected pick orders.")} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end' }}> | |||
| <Button | |||
| variant="outlined" | |||
| onClick={() => setModalOpen(false)} | |||
| > | |||
| <Box sx={{ display: "flex", gap: 2, justifyContent: "flex-end" }}> | |||
| <Button variant="outlined" onClick={() => setModalOpen(false)}> | |||
| {t("Cancel")} | |||
| </Button> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| color="primary" | |||
| > | |||
| {t("Assign & Release")} | |||
| <Button type="submit" variant="contained" color="primary"> | |||
| {t("Assign")} | |||
| </Button> | |||
| </Box> | |||
| </Grid> | |||
| @@ -25,6 +25,7 @@ import AssignAndRelease from "./AssignAndRelease"; | |||
| import AssignTo from "./assignTo"; | |||
| import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | |||
| import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; | |||
| import Jobcreatitem from "./Jobcreatitem"; | |||
| interface Props { | |||
| pickOrders: PickOrderResult[]; | |||
| @@ -224,6 +225,12 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| } | |||
| }, [isOpenCreateModal]) | |||
| // 添加处理提料单创建成功的函数 | |||
| const handlePickOrderCreated = useCallback(() => { | |||
| // 切换到 Assign & Release 标签页 (tabIndex = 1) | |||
| setTabIndex(1); | |||
| }, []); | |||
| return ( | |||
| <> | |||
| <Stack | |||
| @@ -313,24 +320,32 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||
| <Tab label={t("Select Items")} iconPosition="end" /> | |||
| <Tab label={t("Select Items")} iconPosition="end" /> | |||
| <Tab label={t("Select Job Order Items")} iconPosition="end" /> | |||
| <Tab label={t("Assign")} iconPosition="end" /> | |||
| <Tab label={t("Release")} iconPosition="end" /> | |||
| <Tab label={t("Pick Execution")} iconPosition="end" /> | |||
| <Tab label={t("Pick Orders")} iconPosition="end" /> | |||
| <Tab label={t("Consolidated Pick Orders")} iconPosition="end" /> | |||
| {/*<Tab label={t("Pick Orders")} iconPosition="end" />*/} | |||
| {/*<Tab label={t("Consolidated Pick Orders")} iconPosition="end" />*/} | |||
| </Tabs> | |||
| {tabIndex === 4 && ( | |||
| {/*{tabIndex === 4 && ( | |||
| <PickOrders | |||
| filteredPickOrders={filteredPickOrders} | |||
| filterArgs={filterArgs} | |||
| /> | |||
| )}*/} | |||
| {/*{tabIndex === 5 && <ConsolidatedPickOrders filterArgs={filterArgs} />}*/} | |||
| {tabIndex === 4 && <PickExecution filterArgs={filterArgs} />} | |||
| {tabIndex === 0 && ( | |||
| <NewCreateItem | |||
| filterArgs={filterArgs} | |||
| searchQuery={searchQuery} | |||
| onPickOrderCreated={handlePickOrderCreated} | |||
| /> | |||
| )} | |||
| {tabIndex === 5 && <ConsolidatedPickOrders filterArgs={filterArgs} />} | |||
| {tabIndex === 3 && <PickExecution filterArgs={filterArgs} />} | |||
| {tabIndex === 0 && <NewCreateItem filterArgs={filterArgs} searchQuery={searchQuery} />} | |||
| {tabIndex === 1 && <AssignAndRelease filterArgs={filterArgs} />} | |||
| {tabIndex === 2 && <AssignTo filterArgs={filterArgs} />} | |||
| {tabIndex === 1 && <Jobcreatitem filterArgs={filterArgs} />} | |||
| {tabIndex === 2&& <AssignAndRelease filterArgs={filterArgs} />} | |||
| {tabIndex === 3 && <AssignTo filterArgs={filterArgs} />} | |||
| </> | |||
| ); | |||
| }; | |||
| @@ -11,24 +11,33 @@ import { | |||
| ModalProps, | |||
| Stack, | |||
| Typography, | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| TableContainer, | |||
| TableHead, | |||
| TableRow, | |||
| Paper, | |||
| TextField, | |||
| Radio, | |||
| RadioGroup, | |||
| FormControlLabel, | |||
| FormControl, | |||
| Tab, | |||
| Tabs, | |||
| TabsProps, | |||
| Paper, | |||
| } from "@mui/material"; | |||
| import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; | |||
| import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { dummyQCData, QcData } from "../PoDetail/dummyQcTemplate"; | |||
| import { dummyQCData } from "../PoDetail/dummyQcTemplate"; | |||
| import StyledDataGrid from "../StyledDataGrid"; | |||
| import { GridColDef } from "@mui/x-data-grid"; | |||
| import { submitDialogWithWarning } from "../Swal/CustomAlerts"; | |||
| import EscalationLogTable from "../DashboardPage/escalation/EscalationLogTable"; | |||
| import EscalationComponent from "../PoDetail/EscalationComponent"; | |||
| import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions"; | |||
| // Define QcData interface locally | |||
| interface ExtendedQcItem extends QcItemWithChecks { | |||
| qcPassed?: boolean; | |||
| failQty?: number; | |||
| remarks?: string; | |||
| } | |||
| const style = { | |||
| position: "absolute", | |||
| @@ -40,10 +49,11 @@ const style = { | |||
| px: 5, | |||
| pb: 10, | |||
| display: "block", | |||
| width: { xs: "60%", sm: "60%", md: "60%" }, | |||
| width: { xs: "80%", sm: "80%", md: "80%" }, | |||
| maxHeight: "90vh", | |||
| overflowY: "auto", | |||
| }; | |||
| interface CommonProps extends Omit<ModalProps, "children"> { | |||
| itemDetail: GetPickOrderLineInfo & { | |||
| pickOrderCode: string; | |||
| @@ -67,31 +77,47 @@ interface Props extends CommonProps { | |||
| pickOrderCode: string; | |||
| qcResult?: PurchaseQcResult[] | |||
| }; | |||
| qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem | |||
| setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>; // Change to ExtendedQcItem | |||
| } | |||
| const PickQcStockInModalVer2: React.FC<Props> = ({ | |||
| const PickQcStockInModalVer3: React.FC<Props> = ({ | |||
| open, | |||
| onClose, | |||
| itemDetail, | |||
| setItemDetail, | |||
| qc, | |||
| warehouse, | |||
| qcItems, | |||
| setQcItems, | |||
| }) => { | |||
| console.log(warehouse); | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation("pickOrder"); | |||
| const [qcItems, setQcItems] = useState(dummyQCData) | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| //const [qcItems, setQcItems] = useState<QcData[]>(dummyQCData); | |||
| const [isCollapsed, setIsCollapsed] = useState<boolean>(true); | |||
| const [isSubmitting, setIsSubmitting] = useState(false); | |||
| const [feedbackMessage, setFeedbackMessage] = useState<string>(""); | |||
| // Add state to store submitted data | |||
| const [submittedData, setSubmittedData] = useState<any[]>([]); | |||
| const formProps = useForm<any>({ | |||
| defaultValues: { | |||
| qcAccept: true, | |||
| acceptQty: itemDetail.requiredQty ?? 0, | |||
| qcDecision: "1", // Default to accept | |||
| ...itemDetail, | |||
| }, | |||
| }); | |||
| const { control, register, formState: { errors }, watch, setValue } = formProps; | |||
| const qcDecision = watch("qcDecision"); | |||
| const accQty = watch("acceptQty"); | |||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | |||
| (...args) => { | |||
| onClose?.(...args); | |||
| @@ -99,228 +125,368 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||
| [onClose], | |||
| ); | |||
| // QC submission handler | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [], | |||
| ); | |||
| // Save failed QC results only | |||
| const saveQcResults = async (qcData: any) => { | |||
| try { | |||
| const qcResults = qcData.qcItems | |||
| .map((item: any) => ({ | |||
| qcItemId: item.id, | |||
| itemId: itemDetail.itemId, | |||
| stockInLineId: null, | |||
| stockOutLineId: 1, // Fixed to 1 as requested | |||
| failQty: item.isPassed ? 0 : (item.failQty || 0), // 0 for passed, actual qty for failed | |||
| type: "pick_order_qc", | |||
| remarks: item.remarks || "", | |||
| qcPassed: item.isPassed, // ✅ This will now be included | |||
| })); | |||
| // Store the submitted data for debug display | |||
| setSubmittedData(qcResults); | |||
| console.log("Saving QC results:", qcResults); | |||
| // Use the corrected API function instead of manual fetch | |||
| for (const qcResult of qcResults) { | |||
| const response = await savePickOrderQcResult(qcResult); | |||
| console.log("QC Result save success:", response); | |||
| // Check if the response indicates success | |||
| if (!response.id) { | |||
| throw new Error(`Failed to save QC result: ${response.message || 'Unknown error'}`); | |||
| } | |||
| } | |||
| return true; | |||
| } catch (error) { | |||
| console.error("Error saving QC results:", error); | |||
| return false; | |||
| } | |||
| }; | |||
| // Submit with QcComponent-style decision handling | |||
| const onSubmitQc = useCallback<SubmitHandler<any>>( | |||
| async (data, event) => { | |||
| console.log("QC Submission:", event!.nativeEvent); | |||
| setIsSubmitting(true); | |||
| // Get QC data from the shared form context | |||
| const qcAccept = data.qcAccept; | |||
| const acceptQty = data.acceptQty; | |||
| // Validate QC data | |||
| const validationErrors : string[] = []; | |||
| // Check if all QC items have results | |||
| const itemsWithoutResult = qcItems.filter(item => item.isPassed === undefined); | |||
| if (itemsWithoutResult.length > 0) { | |||
| validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.qcItem).join(', ')}`); | |||
| } | |||
| try { | |||
| const qcAccept = qcDecision === "1"; | |||
| const acceptQty = Number(accQty) || itemDetail.requiredQty; | |||
| // Check if failed items have failed quantity | |||
| const failedItemsWithoutQty = qcItems.filter(item => | |||
| item.isPassed === false && (!item.failedQty || item.failedQty <= 0) | |||
| ); | |||
| if (failedItemsWithoutQty.length > 0) { | |||
| validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.qcItem).join(', ')}`); | |||
| } | |||
| const validationErrors : string[] = []; | |||
| // Check if accept quantity is valid | |||
| if (acceptQty === undefined || acceptQty <= 0) { | |||
| validationErrors.push("Accept quantity must be greater than 0"); | |||
| } | |||
| const itemsWithoutResult = qcItems.filter(item => item.qcPassed === undefined); | |||
| if (itemsWithoutResult.length > 0) { | |||
| validationErrors.push(`${t("QC items without result")}: ${itemsWithoutResult.map(item => item.code).join(", ")}`); | |||
| } | |||
| if (validationErrors.length > 0) { | |||
| console.error("QC Validation failed:", validationErrors); | |||
| alert(`QC failed: ${validationErrors}`); | |||
| return; | |||
| } | |||
| const failedItemsWithoutQty = qcItems.filter(item => | |||
| item.qcPassed === false && (!item.failQty || item.failQty <= 0) | |||
| ); | |||
| if (failedItemsWithoutQty.length > 0) { | |||
| validationErrors.push(`${t("Failed items must have failed quantity")}: ${failedItemsWithoutQty.map(item => item.code).join(", ")}`); | |||
| } | |||
| const qcData = { | |||
| qcAccept: qcAccept, | |||
| acceptQty: acceptQty, | |||
| qcItems: qcItems.map(item => ({ | |||
| id: item.id, | |||
| qcItem: item.qcItem, | |||
| qcDescription: item.qcDescription, | |||
| isPassed: item.isPassed, | |||
| failedQty: (item.failedQty && !item.isPassed) || 0, | |||
| remarks: item.remarks || '' | |||
| })) | |||
| }; | |||
| if (qcDecision === "1" && (acceptQty === undefined || acceptQty <= 0)) { | |||
| validationErrors.push("Accept quantity must be greater than 0"); | |||
| } | |||
| console.log("QC Data for submission:", qcData); | |||
| // await submitQcData(qcData); | |||
| if (validationErrors.length > 0) { | |||
| alert(`QC failed: ${validationErrors.join(", ")}`); | |||
| return; | |||
| } | |||
| if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { | |||
| submitDialogWithWarning(() => { | |||
| console.log("QC accepted with failed items"); | |||
| onClose?.(); | |||
| }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); | |||
| return; | |||
| } | |||
| const qcData = { | |||
| qcAccept, | |||
| acceptQty, | |||
| qcItems: qcItems.map(item => ({ | |||
| id: item.id, | |||
| qcItem: item.code, // Use code instead of qcItem | |||
| qcDescription: item.description || "", // Use description instead of qcDescription | |||
| isPassed: item.qcPassed, | |||
| failQty: item.qcPassed ? 0 : (item.failQty ?? 0), | |||
| remarks: item.remarks || "", | |||
| })), | |||
| }; | |||
| if (qcData.qcAccept) { | |||
| console.log("QC accepted"); | |||
| onClose?.(); | |||
| } else { | |||
| console.log("QC rejected"); | |||
| onClose?.(); | |||
| console.log("Submitting QC data:", qcData); | |||
| const saveSuccess = await saveQcResults(qcData); | |||
| if (!saveSuccess) { | |||
| alert("Failed to save QC results"); | |||
| return; | |||
| } | |||
| // Show success message | |||
| alert("QC results saved successfully!"); | |||
| if (!qcData.qcItems.every((q) => q.isPassed) && qcData.qcAccept) { | |||
| submitDialogWithWarning(() => { | |||
| closeHandler?.({}, 'escapeKeyDown'); | |||
| }, t, {title:"有不合格檢查項目,確認接受出庫?", confirmButtonText: "Confirm", html: ""}); | |||
| return; | |||
| } | |||
| closeHandler?.({}, 'escapeKeyDown'); | |||
| } catch (error) { | |||
| console.error("Error in QC submission:", error); | |||
| alert("Error saving QC results: " + (error as Error).message); | |||
| } finally { | |||
| setIsSubmitting(false); | |||
| } | |||
| }, | |||
| [qcItems, onClose, t], | |||
| [qcItems, closeHandler, t, itemDetail, qcDecision, accQty], | |||
| ); | |||
| const handleQcItemChange = useCallback((index: number, field: keyof QcData, value: any) => { | |||
| setQcItems(prev => prev.map((item, i) => | |||
| i === index ? { ...item, [field]: value } : item | |||
| )); | |||
| }, []); | |||
| // DataGrid columns (QcComponent style) | |||
| const qcColumns: GridColDef[] = useMemo( | |||
| () => [ | |||
| { | |||
| field: "code", | |||
| headerName: t("qcItem"), | |||
| flex: 2, | |||
| renderCell: (params) => ( | |||
| <Box> | |||
| <b>{`${params.api.getRowIndexRelativeToVisibleRows(params.id) + 1}. ${params.value}`}</b><br/> | |||
| {params.row.name}<br/> | |||
| </Box> | |||
| ), | |||
| }, | |||
| { | |||
| field: "qcPassed", | |||
| headerName: t("qcResult"), | |||
| flex: 1.5, | |||
| renderCell: (params) => { | |||
| const current = params.row; | |||
| return ( | |||
| <FormControl> | |||
| <RadioGroup | |||
| row | |||
| aria-labelledby="qc-result" | |||
| value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")} | |||
| onChange={(e) => { | |||
| const value = e.target.value === "true"; | |||
| setQcItems((prev) => | |||
| prev.map((r): ExtendedQcItem => (r.id === params.id ? { ...r, qcPassed: value } : r)) | |||
| ); | |||
| }} | |||
| name={`qcPassed-${params.id}`} | |||
| > | |||
| <FormControlLabel | |||
| value="true" | |||
| control={<Radio size="small" />} | |||
| label="合格" | |||
| sx={{ | |||
| color: current.qcPassed === true ? "green" : "inherit", | |||
| "& .Mui-checked": {color: "green"} | |||
| }} | |||
| /> | |||
| <FormControlLabel | |||
| value="false" | |||
| control={<Radio size="small" />} | |||
| label="不合格" | |||
| sx={{ | |||
| color: current.qcPassed === false ? "red" : "inherit", | |||
| "& .Mui-checked": {color: "red"} | |||
| }} | |||
| /> | |||
| </RadioGroup> | |||
| </FormControl> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| field: "failQty", | |||
| headerName: t("failedQty"), | |||
| flex: 1, | |||
| renderCell: (params) => ( | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| value={!params.row.qcPassed ? (params.value ?? "") : "0"} | |||
| 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)) | |||
| ); | |||
| }} | |||
| onClick={(e) => e.stopPropagation()} | |||
| onMouseDown={(e) => e.stopPropagation()} | |||
| onKeyDown={(e) => e.stopPropagation()} | |||
| inputProps={{ min: 0 }} | |||
| sx={{ width: "100%" }} | |||
| /> | |||
| ), | |||
| }, | |||
| { | |||
| field: "remarks", | |||
| headerName: t("remarks"), | |||
| flex: 2, | |||
| renderCell: (params) => ( | |||
| <TextField | |||
| size="small" | |||
| value={params.value ?? ""} | |||
| onChange={(e) => { | |||
| const remarks = e.target.value; | |||
| setQcItems((prev) => | |||
| prev.map((r) => (r.id === params.id ? { ...r, remarks } : r)) | |||
| ); | |||
| }} | |||
| onClick={(e) => e.stopPropagation()} | |||
| onMouseDown={(e) => e.stopPropagation()} | |||
| onKeyDown={(e) => e.stopPropagation()} | |||
| sx={{ width: "100%" }} | |||
| /> | |||
| ), | |||
| }, | |||
| ], | |||
| [t], | |||
| ); | |||
| return ( | |||
| <> | |||
| <FormProvider {...formProps}> | |||
| <Modal open={open} onClose={closeHandler}> | |||
| <Box | |||
| sx={{ | |||
| ...style, | |||
| padding: 2, | |||
| maxHeight: "90vh", | |||
| overflowY: "auto", | |||
| marginLeft: 3, | |||
| marginRight: 3, | |||
| }} | |||
| > | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start"> | |||
| <Box sx={style}> | |||
| <Grid container justifyContent="flex-start" alignItems="flex-start" spacing={2}> | |||
| <Grid item xs={12}> | |||
| <Typography variant="h6" display="block" marginBlockEnd={1}> | |||
| GroupA - {itemDetail.pickOrderCode} | |||
| </Typography> | |||
| <Typography variant="body2" color="text.secondary" marginBlockEnd={2}> | |||
| 記錄探測溫度的時間,請在1小時內完成出庫,以保障食品安全 監察方法、日闸檢查、嗅覺檢查和使用適當的食物温度計椒鱼食物溫度是否符合指標 | |||
| </Typography> | |||
| <Tabs | |||
| value={tabIndex} | |||
| onChange={handleTabChange} | |||
| variant="scrollable" | |||
| > | |||
| <Tab label={t("QC Info")} iconPosition="end" /> | |||
| <Tab label={t("Escalation History")} iconPosition="end" /> | |||
| </Tabs> | |||
| </Grid> | |||
| {/* QC table - same as QcFormVer2 */} | |||
| {tabIndex == 0 && ( | |||
| <> | |||
| <Grid item xs={12}> | |||
| <Box sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5', borderRadius: 1 }}> | |||
| <Typography variant="h5" component="h2" sx={{ fontWeight: 'bold', color: '#333' }}> | |||
| Group A - 急凍貨類 (QCA1-MEAT01) | |||
| </Typography> | |||
| <Typography variant="subtitle1" sx={{ color: '#666' }}> | |||
| <b>品檢類型</b>:OQC | |||
| </Typography> | |||
| <Typography variant="subtitle2" sx={{ color: '#666' }}> | |||
| 記錄探測溫度的時間,請在1小時内完成出庫盤點,以保障食品安全<br/> | |||
| 監察方法:目視檢查、嗅覺檢查和使用適當的食物溫度計,檢查食物溫度是否符合指標 | |||
| </Typography> | |||
| </Box> | |||
| <StyledDataGrid | |||
| columns={qcColumns} | |||
| rows={qcItems} | |||
| autoHeight | |||
| /> | |||
| </Grid> | |||
| </> | |||
| )} | |||
| {tabIndex == 1 && ( | |||
| <> | |||
| <Grid item xs={12}> | |||
| <EscalationLogTable items={[]}/> | |||
| </Grid> | |||
| </> | |||
| )} | |||
| <Grid item xs={12}> | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell sx={{ width: '80px' }}>QC模板代號</TableCell> | |||
| <TableCell sx={{ width: '300px' }}>檢查項目</TableCell> | |||
| <TableCell sx={{ width: '120px' }}>QC RESULT</TableCell> | |||
| <TableCell sx={{ width: '80px' }}>FAILED QTY</TableCell> | |||
| <TableCell sx={{ width: '300px' }}>REMARKS</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {qcItems.map((item, index) => ( | |||
| <TableRow key={item.id}> | |||
| <TableCell>{item.id}</TableCell> | |||
| <TableCell sx={{ | |||
| maxWidth: '300px', | |||
| wordWrap: 'break-word', | |||
| whiteSpace: 'normal' | |||
| }}> | |||
| {item.qcDescription} | |||
| </TableCell> | |||
| <TableCell> | |||
| {/* same as QcFormVer2 */} | |||
| <FormControl> | |||
| <RadioGroup | |||
| row | |||
| aria-labelledby="demo-radio-buttons-group-label" | |||
| value={item.isPassed === undefined ? "" : (item.isPassed ? "true" : "false")} | |||
| onChange={(e) => { | |||
| const value = e.target.value; | |||
| handleQcItemChange(index, 'isPassed', value === "true"); | |||
| }} | |||
| name={`isPassed-${item.id}`} | |||
| > | |||
| <FormControlLabel | |||
| value="true" | |||
| control={<Radio size="small" />} | |||
| label="合格" | |||
| sx={{ | |||
| color: item.isPassed === true ? "green" : "inherit", | |||
| "& .Mui-checked": {color: "green"} | |||
| }} | |||
| /> | |||
| <FormControlLabel | |||
| value="false" | |||
| control={<Radio size="small" />} | |||
| label="不合格" | |||
| sx={{ | |||
| color: item.isPassed === false ? "red" : "inherit", | |||
| "& .Mui-checked": {color: "red"} | |||
| }} | |||
| /> | |||
| </RadioGroup> | |||
| </FormControl> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| type="number" | |||
| size="small" | |||
| value={!item.isPassed ? (item.failedQty ?? 0) : 0} | |||
| disabled={item.isPassed} | |||
| onChange={(e) => { | |||
| const v = e.target.value; | |||
| const next = v === '' ? undefined : Number(v); | |||
| if (Number.isNaN(next)) return; | |||
| handleQcItemChange(index, 'failedQty', next); | |||
| }} | |||
| inputProps={{ min: 0 }} | |||
| sx={{ width: '60px' }} | |||
| /> | |||
| </TableCell> | |||
| <TableCell> | |||
| <TextField | |||
| size="small" | |||
| value={item.remarks ?? ''} | |||
| onChange={(e) => { | |||
| const remarks = e.target.value; | |||
| handleQcItemChange(index, 'remarks', remarks); | |||
| }} | |||
| sx={{ width: '280px' }} | |||
| /> | |||
| </TableCell> | |||
| </TableRow> | |||
| ))} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| <FormControl> | |||
| <Controller | |||
| name="qcDecision" | |||
| control={control} | |||
| defaultValue="1" | |||
| render={({ field }) => ( | |||
| <RadioGroup | |||
| row | |||
| aria-labelledby="demo-radio-buttons-group-label" | |||
| {...field} | |||
| value={field.value} | |||
| onChange={(e) => { | |||
| const value = e.target.value.toString(); | |||
| if (value != "1" && Boolean(errors.acceptQty)) { | |||
| setValue("acceptQty", itemDetail.requiredQty ?? 0); | |||
| } | |||
| field.onChange(value); | |||
| }} | |||
| > | |||
| <FormControlLabel | |||
| value="1" | |||
| control={<Radio />} | |||
| label="接受出庫" | |||
| /> | |||
| <Box sx={{mr:2}}> | |||
| <TextField | |||
| type="number" | |||
| label={t("acceptQty")} | |||
| sx={{ width: '150px' }} | |||
| value={(qcDecision == 1)? accQty : 0 } | |||
| disabled={qcDecision != 1} | |||
| {...register("acceptQty", { | |||
| required: "acceptQty required!", | |||
| })} | |||
| error={Boolean(errors.acceptQty)} | |||
| helperText={errors.acceptQty?.message?.toString() || ""} | |||
| /> | |||
| </Box> | |||
| <FormControlLabel | |||
| value="2" | |||
| control={<Radio />} | |||
| sx={{"& .Mui-checked": {color: "red"}}} | |||
| label="不接受並重新揀貨" | |||
| /> | |||
| <FormControlLabel | |||
| value="3" | |||
| control={<Radio />} | |||
| sx={{"& .Mui-checked": {color: "blue"}}} | |||
| label="上報品檢結果" | |||
| /> | |||
| </RadioGroup> | |||
| )} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| {/* buttons */} | |||
| {qcDecision == 3 && ( | |||
| <Grid item xs={12}> | |||
| <EscalationComponent | |||
| forSupervisor={false} | |||
| isCollapsed={isCollapsed} | |||
| setIsCollapsed={setIsCollapsed} | |||
| /> | |||
| </Grid> | |||
| )} | |||
| <Grid item xs={12} sx={{ mt: 2 }}> | |||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | |||
| <Button | |||
| variant="contained" | |||
| onClick={formProps.handleSubmit(onSubmitQc)} | |||
| disabled={isSubmitting} | |||
| sx={{ whiteSpace: 'nowrap' }} | |||
| > | |||
| QC Accept | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| onClick={() => { | |||
| console.log("Sort to accept"); | |||
| onClose?.(); | |||
| }} | |||
| > | |||
| Sort to Accept | |||
| {isSubmitting ? "Submitting..." : "Submit QC"} | |||
| </Button> | |||
| <Button | |||
| variant="contained" | |||
| variant="outlined" | |||
| onClick={() => { | |||
| console.log("Reject and pick another lot"); | |||
| onClose?.(); | |||
| closeHandler?.({}, 'escapeKeyDown'); | |||
| }} | |||
| > | |||
| Reject and Pick Another Lot | |||
| Cancel | |||
| </Button> | |||
| </Stack> | |||
| </Grid> | |||
| @@ -332,4 +498,4 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||
| ); | |||
| }; | |||
| export default PickQcStockInModalVer2; | |||
| export default PickQcStockInModalVer3; | |||
| @@ -4,93 +4,381 @@ import { | |||
| Box, | |||
| Button, | |||
| CircularProgress, | |||
| FormControl, | |||
| Grid, | |||
| Stack, | |||
| Modal, | |||
| TextField, | |||
| Typography, | |||
| Table, | |||
| TableBody, | |||
| TableCell, | |||
| TableContainer, | |||
| TableHead, | |||
| TableRow, | |||
| Paper, | |||
| Checkbox, | |||
| TablePagination, | |||
| } from "@mui/material"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import SearchResults, { Column } from "../SearchResults/SearchResults"; | |||
| import { fetchConsoPickOrderClient } from "@/app/api/pickOrder/actions"; | |||
| import { | |||
| newassignPickOrder, | |||
| AssignPickOrderInputs, | |||
| releaseAssignedPickOrders, | |||
| } from "@/app/api/pickOrder/actions"; | |||
| import { fetchNameList, NameList } from "@/app/api/user/actions"; | |||
| import { isEmpty, upperFirst } from "lodash"; | |||
| import { arrayToDateString } from "@/app/utils/formatUtil"; | |||
| import { | |||
| FormProvider, | |||
| useForm, | |||
| } from "react-hook-form"; | |||
| import { isEmpty, upperFirst, groupBy } from "lodash"; | |||
| import { OUTPUT_DATE_FORMAT, arrayToDayjs } from "@/app/utils/formatUtil"; | |||
| import useUploadContext from "../UploadProvider/useUploadContext"; | |||
| import dayjs from "dayjs"; | |||
| import arraySupport from "dayjs/plugin/arraySupport"; | |||
| import SearchBox, { Criterion } from "../SearchBox"; | |||
| import { sortBy, uniqBy } from "lodash"; | |||
| import { fetchPickOrderItemsByPageClient } from "@/app/api/settings/item/actions"; | |||
| dayjs.extend(arraySupport); | |||
| interface Props { | |||
| filterArgs: Record<string, any>; | |||
| } | |||
| interface AssignmentData { | |||
| // 使用 fetchPickOrderItemsByPageClient 返回的数据结构 | |||
| interface ItemRow { | |||
| id: string; | |||
| consoCode: string; | |||
| releasedDate: string | null; | |||
| pickOrderId: number; | |||
| pickOrderCode: string; | |||
| itemId: number; | |||
| itemCode: string; | |||
| itemName: string; | |||
| requiredQty: number; | |||
| currentStock: number; | |||
| unit: string; | |||
| targetDate: any; | |||
| status: string; | |||
| consoCode?: string; | |||
| assignTo?: number; | |||
| groupName?: string; | |||
| } | |||
| // 分组后的数据结构 | |||
| interface GroupedItemRow { | |||
| pickOrderId: number; | |||
| pickOrderCode: string; | |||
| targetDate: any; | |||
| status: string; | |||
| assignTo: number | null; | |||
| assignedUserName?: string; | |||
| consoCode?: string; | |||
| items: ItemRow[]; | |||
| } | |||
| const style = { | |||
| position: "absolute", | |||
| top: "50%", | |||
| left: "50%", | |||
| transform: "translate(-50%, -50%)", | |||
| bgcolor: "background.paper", | |||
| pt: 5, | |||
| px: 5, | |||
| pb: 10, | |||
| width: { xs: "100%", sm: "100%", md: "100%" }, | |||
| }; | |||
| const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||
| const { t } = useTranslation("pickOrder"); | |||
| // State | |||
| const [assignmentData, setAssignmentData] = useState<AssignmentData[]>([]); | |||
| const [isLoading, setIsLoading] = useState(false); | |||
| const { setIsUploading } = useUploadContext(); | |||
| // 修复:选择状态改为按 pick order ID 存储 | |||
| const [selectedPickOrderIds, setSelectedPickOrderIds] = useState<number[]>([]); | |||
| const [filteredItems, setFilteredItems] = useState<ItemRow[]>([]); | |||
| const [isLoadingItems, setIsLoadingItems] = useState(false); | |||
| const [pagingController, setPagingController] = useState({ | |||
| pageNum: 1, | |||
| pageSize: 50, | |||
| pageSize: 10, | |||
| }); | |||
| const [totalCount, setTotalCount] = useState<number>(); | |||
| const [totalCountItems, setTotalCountItems] = useState<number>(); | |||
| const [modalOpen, setModalOpen] = useState(false); | |||
| const [usernameList, setUsernameList] = useState<NameList[]>([]); | |||
| const [selectedUser, setSelectedUser] = useState<NameList | null>(null); | |||
| const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); | |||
| const [originalItemData, setOriginalItemData] = useState<ItemRow[]>([]); | |||
| const formProps = useForm<AssignPickOrderInputs>(); | |||
| const errors = formProps.formState.errors; | |||
| // 将项目按 pick order 分组 | |||
| const groupedItems = useMemo(() => { | |||
| const grouped = groupBy(filteredItems, 'pickOrderId'); | |||
| return Object.entries(grouped).map(([pickOrderId, items]) => { | |||
| const firstItem = items[0]; | |||
| return { | |||
| pickOrderId: parseInt(pickOrderId), | |||
| pickOrderCode: firstItem.pickOrderCode, | |||
| targetDate: firstItem.targetDate, | |||
| status: firstItem.status, | |||
| consoCode: firstItem.consoCode, | |||
| items: items, | |||
| groupName: firstItem.groupName, | |||
| } as GroupedItemRow; | |||
| }); | |||
| }, [filteredItems]); | |||
| // 修复:处理 pick order 选择 | |||
| const handlePickOrderSelect = useCallback((pickOrderId: number, checked: boolean) => { | |||
| if (checked) { | |||
| setSelectedPickOrderIds(prev => [...prev, pickOrderId]); | |||
| } else { | |||
| setSelectedPickOrderIds(prev => prev.filter(id => id !== pickOrderId)); | |||
| } | |||
| }, []); | |||
| // 修复:检查 pick order 是否被选中 | |||
| const isPickOrderSelected = useCallback((pickOrderId: number) => { | |||
| return selectedPickOrderIds.includes(pickOrderId); | |||
| }, [selectedPickOrderIds]); | |||
| // 使用 fetchPickOrderItemsByPageClient 获取数据 | |||
| const fetchNewPageItems = useCallback( | |||
| async (pagingController: Record<string, number>, filterArgs: Record<string, any>) => { | |||
| console.log("=== fetchNewPageItems called ==="); | |||
| console.log("pagingController:", pagingController); | |||
| console.log("filterArgs:", filterArgs); | |||
| setIsLoadingItems(true); | |||
| try { | |||
| const params = { | |||
| ...pagingController, | |||
| ...filterArgs, | |||
| // 新增:只获取状态为 "assigned" 的提料单 | |||
| status: "assigned" | |||
| }; | |||
| console.log("Final params:", params); | |||
| const res = await fetchPickOrderItemsByPageClient(params); | |||
| console.log("API Response:", res); | |||
| if (res && res.records) { | |||
| console.log("Records received:", res.records.length); | |||
| console.log("First record:", res.records[0]); | |||
| const itemRows: ItemRow[] = res.records.map((item: any) => ({ | |||
| id: item.id, | |||
| pickOrderId: item.pickOrderId, | |||
| pickOrderCode: item.pickOrderCode, | |||
| itemId: item.itemId, | |||
| itemCode: item.itemCode, | |||
| itemName: item.itemName, | |||
| requiredQty: item.requiredQty, | |||
| currentStock: item.currentStock ?? 0, | |||
| unit: item.unit, | |||
| targetDate: item.targetDate, | |||
| status: item.status, | |||
| consoCode: item.consoCode, | |||
| assignTo: item.assignTo, | |||
| })); | |||
| setOriginalItemData(itemRows); | |||
| setFilteredItems(itemRows); | |||
| setTotalCountItems(res.total); | |||
| } else { | |||
| console.log("No records in response"); | |||
| setFilteredItems([]); | |||
| setTotalCountItems(0); | |||
| } | |||
| } catch (error) { | |||
| console.error("Error fetching items:", error); | |||
| setFilteredItems([]); | |||
| setTotalCountItems(0); | |||
| } finally { | |||
| setIsLoadingItems(false); | |||
| } | |||
| }, | |||
| [], | |||
| ); | |||
| // 新增:处理 Release 操作(包含完整的库存管理) | |||
| const handleRelease = useCallback(async () => { | |||
| if (selectedPickOrderIds.length === 0) return; | |||
| // Fetch assignment data | |||
| const fetchAssignmentData = useCallback(async () => { | |||
| setIsLoading(true); | |||
| setIsUploading(true); | |||
| try { | |||
| const params = { | |||
| ...pagingController, | |||
| ...filterArgs, | |||
| // Add user filter if selected | |||
| ...(selectedUser && { assignTo: selectedUser.id }), | |||
| }; | |||
| // 调用新的 release API,包含完整的库存管理功能 | |||
| const releaseRes = await releaseAssignedPickOrders({ | |||
| pickOrderIds: selectedPickOrderIds, | |||
| assignTo: 0, // 这个参数在 release 时不会被使用 | |||
| }); | |||
| if (releaseRes && releaseRes.code === "SUCCESS") { | |||
| console.log("Release successful with inventory management:", releaseRes); | |||
| setSelectedPickOrderIds([]); // 清空选择 | |||
| fetchNewPageItems(pagingController, filterArgs); | |||
| } else { | |||
| console.error("Release failed:", releaseRes); | |||
| } | |||
| } catch (error) { | |||
| console.error("Error in release:", error); | |||
| } finally { | |||
| setIsUploading(false); | |||
| } | |||
| }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); | |||
| const searchCriteria: Criterion<any>[] = useMemo( | |||
| () => [ | |||
| { | |||
| label: t("Pick Order Code"), | |||
| paramName: "pickOrderCode", | |||
| type: "text", | |||
| }, | |||
| { | |||
| label: t("Item Code"), | |||
| paramName: "itemCode", | |||
| type: "text" | |||
| }, | |||
| { | |||
| label: t("Item Name"), | |||
| paramName: "itemName", | |||
| type: "text", | |||
| }, | |||
| { | |||
| label: t("Target Date From"), | |||
| label2: t("Target Date To"), | |||
| paramName: "targetDate", | |||
| type: "dateRange", | |||
| }, | |||
| ], | |||
| [t], | |||
| ); | |||
| const handleSearch = useCallback((query: Record<string, any>) => { | |||
| setSearchQuery({ ...query }); | |||
| console.log("Search query:", query); | |||
| const filtered = originalItemData.filter((item) => { | |||
| const itemTargetDateStr = arrayToDayjs(item.targetDate); | |||
| const itemCodeMatch = !query.itemCode || | |||
| item.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase()); | |||
| console.log("Fetching with params:", params); | |||
| const itemNameMatch = !query.itemName || | |||
| item.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase()); | |||
| const res = await fetchConsoPickOrderClient(params); | |||
| if (res) { | |||
| console.log("API response:", res); | |||
| // Enhance data with user names and add id | |||
| const enhancedData = res.records.map((record: any, index: number) => { | |||
| const userName = record.assignTo | |||
| ? usernameList.find(user => user.id === record.assignTo)?.name | |||
| : null; | |||
| return { | |||
| ...record, | |||
| id: record.consoCode || `temp-${index}`, | |||
| assignedUserName: userName || 'Unassigned', | |||
| }; | |||
| }); | |||
| setAssignmentData(enhancedData); | |||
| setTotalCount(res.total); | |||
| const pickOrderCodeMatch = !query.pickOrderCode || | |||
| item.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase()); | |||
| // 日期范围搜索 | |||
| let dateMatch = true; | |||
| if (query.targetDate || query.targetDateTo) { | |||
| try { | |||
| if (query.targetDate && !query.targetDateTo) { | |||
| const fromDate = dayjs(query.targetDate); | |||
| dateMatch = itemTargetDateStr.isSame(fromDate, 'day') || | |||
| itemTargetDateStr.isAfter(fromDate, 'day'); | |||
| } else if (!query.targetDate && query.targetDateTo) { | |||
| const toDate = dayjs(query.targetDateTo); | |||
| dateMatch = itemTargetDateStr.isSame(toDate, 'day') || | |||
| itemTargetDateStr.isBefore(toDate, 'day'); | |||
| } else if (query.targetDate && query.targetDateTo) { | |||
| const fromDate = dayjs(query.targetDate); | |||
| const toDate = dayjs(query.targetDateTo); | |||
| dateMatch = (itemTargetDateStr.isSame(fromDate, 'day') || | |||
| itemTargetDateStr.isAfter(fromDate, 'day')) && | |||
| (itemTargetDateStr.isSame(toDate, 'day') || | |||
| itemTargetDateStr.isBefore(toDate, 'day')); | |||
| } | |||
| } catch (error) { | |||
| console.error("Date parsing error:", error); | |||
| dateMatch = true; | |||
| } | |||
| } | |||
| const statusMatch = !query.status || | |||
| query.status.toLowerCase() === "all" || | |||
| item.status?.toLowerCase().includes((query.status || "").toLowerCase()); | |||
| return itemCodeMatch && itemNameMatch && pickOrderCodeMatch && dateMatch && statusMatch; | |||
| }); | |||
| console.log("Filtered items count:", filtered.length); | |||
| setFilteredItems(filtered); | |||
| }, [originalItemData]); | |||
| const handleReset = useCallback(() => { | |||
| setSearchQuery({}); | |||
| setFilteredItems(originalItemData); | |||
| setTimeout(() => { | |||
| setSearchQuery({}); | |||
| }, 0); | |||
| }, [originalItemData]); | |||
| // 修复:处理分页变化 | |||
| const handlePageChange = useCallback((event: unknown, newPage: number) => { | |||
| const newPagingController = { | |||
| ...pagingController, | |||
| pageNum: newPage + 1, // API 使用 1-based 分页 | |||
| }; | |||
| setPagingController(newPagingController); | |||
| }, [pagingController]); | |||
| const handlePageSizeChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => { | |||
| const newPageSize = parseInt(event.target.value, 10); | |||
| const newPagingController = { | |||
| pageNum: 1, // 重置到第一页 | |||
| pageSize: newPageSize, | |||
| }; | |||
| setPagingController(newPagingController); | |||
| }, []); | |||
| const handleAssignAndRelease = useCallback(async (data: AssignPickOrderInputs) => { | |||
| if (selectedPickOrderIds.length === 0) return; | |||
| setIsUploading(true); | |||
| try { | |||
| // 修复:直接使用选中的 pick order IDs | |||
| const assignRes = await newassignPickOrder({ | |||
| pickOrderIds: selectedPickOrderIds, | |||
| assignTo: data.assignTo, | |||
| }); | |||
| if (assignRes && assignRes.code === "SUCCESS") { | |||
| console.log("Assign successful:", assignRes); | |||
| setModalOpen(false); | |||
| setSelectedPickOrderIds([]); // 清空选择 | |||
| fetchNewPageItems(pagingController, filterArgs); | |||
| } else { | |||
| console.error("Assign failed:", assignRes); | |||
| } | |||
| } catch (error) { | |||
| console.error("Error fetching assignment data:", error); | |||
| console.error("Error in assign:", error); | |||
| } finally { | |||
| setIsLoading(false); | |||
| setIsUploading(false); | |||
| } | |||
| }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); | |||
| const openAssignModal = useCallback(() => { | |||
| setModalOpen(true); | |||
| formProps.reset(); | |||
| }, [formProps]); | |||
| // 组件挂载时加载数据 | |||
| useEffect(() => { | |||
| console.log("=== Component mounted ==="); | |||
| fetchNewPageItems(pagingController, filterArgs || {}); | |||
| }, []); // 只在组件挂载时执行一次 | |||
| // 当 pagingController 或 filterArgs 变化时重新调用 API | |||
| useEffect(() => { | |||
| console.log("=== Dependencies changed ==="); | |||
| if (pagingController && (filterArgs || {})) { | |||
| fetchNewPageItems(pagingController, filterArgs || {}); | |||
| } | |||
| }, [pagingController, filterArgs, selectedUser, usernameList]); | |||
| }, [pagingController, filterArgs, fetchNewPageItems]); | |||
| // Load username list | |||
| useEffect(() => { | |||
| const loadUsernameList = async () => { | |||
| try { | |||
| const res = await fetchNameList(); | |||
| if (res) { | |||
| console.log("Loaded username list:", res); | |||
| setUsernameList(res); | |||
| } | |||
| } catch (error) { | |||
| @@ -100,134 +388,157 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||
| loadUsernameList(); | |||
| }, []); | |||
| // Fetch data when dependencies change | |||
| useEffect(() => { | |||
| fetchAssignmentData(); | |||
| }, [fetchAssignmentData]); | |||
| // Handle user selection | |||
| const handleUserChange = useCallback((event: any, newValue: NameList | null) => { | |||
| setSelectedUser(newValue); | |||
| // Reset to first page when filtering | |||
| setPagingController(prev => ({ ...prev, pageNum: 1 })); | |||
| }, []); | |||
| // 自定义分组表格组件 | |||
| const CustomGroupedTable = () => { | |||
| // 获取用户名的辅助函数 | |||
| const getUserName = useCallback((assignToId: number | null | undefined) => { | |||
| if (!assignToId) return '-'; | |||
| const user = usernameList.find(u => u.id === assignToId); | |||
| return user ? user.name : `User ${assignToId}`; | |||
| }, [usernameList]); | |||
| // Clear filter | |||
| const handleClearFilter = useCallback(() => { | |||
| setSelectedUser(null); | |||
| setPagingController(prev => ({ ...prev, pageNum: 1 })); | |||
| }, []); | |||
| return ( | |||
| <> | |||
| <TableContainer component={Paper}> | |||
| <Table> | |||
| <TableHead> | |||
| <TableRow> | |||
| <TableCell>{t("Selected")}</TableCell> | |||
| <TableCell>{t("Pick Order Code")}</TableCell> | |||
| <TableCell>{t("Group Name")}</TableCell> | |||
| <TableCell>{t("Item Code")}</TableCell> | |||
| <TableCell>{t("Item Name")}</TableCell> | |||
| <TableCell align="right">{t("Order Quantity")}</TableCell> | |||
| <TableCell align="right">{t("Current Stock")}</TableCell> | |||
| <TableCell align="right">{t("Stock Unit")}</TableCell> | |||
| <TableCell>{t("Target Date")}</TableCell> | |||
| <TableCell>{t("Assigned To")}</TableCell> | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {groupedItems.length === 0 ? ( | |||
| <TableRow> | |||
| <TableCell colSpan={9} align="center"> | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("No data available")} | |||
| </Typography> | |||
| </TableCell> | |||
| </TableRow> | |||
| ) : ( | |||
| groupedItems.map((group) => ( | |||
| group.items.map((item, index) => ( | |||
| <TableRow key={item.id}> | |||
| {/* Checkbox - 只在第一个项目显示,按 pick order 选择 */} | |||
| <TableCell> | |||
| {index === 0 ? ( | |||
| <Checkbox | |||
| checked={isPickOrderSelected(group.pickOrderId)} | |||
| onChange={(e) => handlePickOrderSelect(group.pickOrderId, e.target.checked)} | |||
| disabled={!isEmpty(item.consoCode)} | |||
| /> | |||
| ) : null} | |||
| </TableCell> | |||
| {/* Pick Order Code - 只在第一个项目显示 */} | |||
| <TableCell> | |||
| {index === 0 ? item.pickOrderCode : null} | |||
| </TableCell> | |||
| {/* Group Name */} | |||
| <TableCell> | |||
| {index === 0 ? (item.groupName || "No Group") : null} | |||
| </TableCell> | |||
| {/* Item Code */} | |||
| <TableCell>{item.itemCode}</TableCell> | |||
| {/* Item Name */} | |||
| <TableCell>{item.itemName}</TableCell> | |||
| // Columns definition | |||
| const columns = useMemo<Column<AssignmentData>[]>( | |||
| () => [ | |||
| { | |||
| name: "consoCode", | |||
| label: t("Consolidated Code"), | |||
| }, | |||
| { | |||
| name: "assignedUserName", | |||
| label: t("Assigned To"), | |||
| renderCell: (params) => { | |||
| if (!params.assignTo) { | |||
| return ( | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Unassigned")} | |||
| </Typography> | |||
| ); | |||
| } | |||
| return ( | |||
| <Typography variant="body2" color="primary"> | |||
| {params.assignedUserName} | |||
| </Typography> | |||
| ); | |||
| }, | |||
| }, | |||
| { | |||
| name: "status", | |||
| label: t("Status"), | |||
| renderCell: (params) => { | |||
| return upperFirst(params.status); | |||
| }, | |||
| }, | |||
| { | |||
| name: "releasedDate", | |||
| label: t("Released Date"), | |||
| renderCell: (params) => { | |||
| if (!params.releasedDate) { | |||
| return ( | |||
| <Typography variant="body2" color="text.secondary"> | |||
| {t("Not Released")} | |||
| </Typography> | |||
| ); | |||
| {/* Order Quantity */} | |||
| <TableCell align="right">{item.requiredQty}</TableCell> | |||
| {/* Current Stock */} | |||
| <TableCell align="right"> | |||
| <Typography | |||
| variant="body2" | |||
| color={item.currentStock > 0 ? "success.main" : "error.main"} | |||
| sx={{ fontWeight: item.currentStock > 0 ? 'bold' : 'normal' }} | |||
| > | |||
| {item.currentStock.toLocaleString()} | |||
| </Typography> | |||
| </TableCell> | |||
| {/* Unit */} | |||
| <TableCell align="right">{item.unit}</TableCell> | |||
| {/* Target Date - 只在第一个项目显示 */} | |||
| <TableCell> | |||
| {index === 0 ? ( | |||
| arrayToDayjs(item.targetDate) | |||
| .add(-1, "month") | |||
| .format(OUTPUT_DATE_FORMAT) | |||
| ) : null} | |||
| </TableCell> | |||
| {/* Assigned To - 只在第一个项目显示,显示用户名 */} | |||
| <TableCell> | |||
| {index === 0 ? ( | |||
| <Typography variant="body2"> | |||
| {getUserName(item.assignTo)} | |||
| </Typography> | |||
| ) : null} | |||
| </TableCell> | |||
| </TableRow> | |||
| )) | |||
| )) | |||
| )} | |||
| </TableBody> | |||
| </Table> | |||
| </TableContainer> | |||
| {/* 修复:添加分页组件 */} | |||
| <TablePagination | |||
| component="div" | |||
| count={totalCountItems || 0} | |||
| page={(pagingController.pageNum - 1)} // 转换为 0-based | |||
| rowsPerPage={pagingController.pageSize} | |||
| onPageChange={handlePageChange} | |||
| onRowsPerPageChange={handlePageSizeChange} | |||
| rowsPerPageOptions={[10, 25, 50]} | |||
| labelRowsPerPage={t("Rows per page")} | |||
| labelDisplayedRows={({ from, to, count }) => | |||
| `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` | |||
| } | |||
| return arrayToDateString(params.releasedDate); | |||
| }, | |||
| }, | |||
| ], | |||
| [t], | |||
| ); | |||
| /> | |||
| </> | |||
| ); | |||
| }; | |||
| return ( | |||
| <Stack spacing={2}> | |||
| {/* Filter Section */} | |||
| <Grid container spacing={2}> | |||
| <Grid item xs={4}> | |||
| <Autocomplete | |||
| options={usernameList} | |||
| getOptionLabel={(option) => option.name} | |||
| value={selectedUser} | |||
| onChange={handleUserChange} | |||
| renderInput={(params) => ( | |||
| <TextField | |||
| {...params} | |||
| label={t("Select User to Filter")} | |||
| variant="outlined" | |||
| fullWidth | |||
| /> | |||
| )} | |||
| renderOption={(props, option) => ( | |||
| <Box component="li" {...props}> | |||
| <Typography variant="body2"> | |||
| {option.name} (ID: {option.id}) | |||
| </Typography> | |||
| </Box> | |||
| )} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={2}> | |||
| <Button | |||
| variant="outlined" | |||
| onClick={handleClearFilter} | |||
| disabled={!selectedUser} | |||
| > | |||
| {t("Clear Filter")} | |||
| </Button> | |||
| </Grid> | |||
| </Grid> | |||
| {/* Data Table - Match PickExecution exactly */} | |||
| <Grid container spacing={2} sx={{ height: '100%', flex: 1 }}> | |||
| <Grid item xs={12} sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}> | |||
| {isLoading ? ( | |||
| <Box display="flex" justifyContent="center" alignItems="center" flex={1}> | |||
| <CircularProgress size={40} /> | |||
| </Box> | |||
| <> | |||
| <SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} /> | |||
| <Grid container rowGap={1}> | |||
| <Grid item xs={12}> | |||
| {isLoadingItems ? ( | |||
| <CircularProgress size={40} /> | |||
| ) : ( | |||
| <SearchResults<AssignmentData> | |||
| items={assignmentData} | |||
| columns={columns} | |||
| pagingController={pagingController} | |||
| setPagingController={setPagingController} | |||
| totalCount={totalCount} | |||
| /> | |||
| <CustomGroupedTable /> | |||
| )} | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Box sx={{ display: "flex", justifyContent: "flex-start", mt: 2 }}> | |||
| <Button | |||
| disabled={selectedPickOrderIds.length < 1} | |||
| variant="outlined" | |||
| onClick={handleRelease} | |||
| > | |||
| {t("Release")} | |||
| </Button> | |||
| </Box> | |||
| </Grid> | |||
| </Grid> | |||
| </Stack> | |||
| </> | |||
| ); | |||
| }; | |||
| export default AssignTo; | |||
| export default AssignTo; | |||
| @@ -7,9 +7,10 @@ | |||
| "Details": "詳情", | |||
| "Supplier": "供應商", | |||
| "Status": "來貨狀態", | |||
| "Release Pick Orders": "放單", | |||
| "Escalated": "上報狀態", | |||
| "NotEscalated": "無上報", | |||
| "Assigned To": "已分配", | |||
| "Do you want to start?": "確定開始嗎?", | |||
| "Start": "開始", | |||
| "Start Success": "開始成功", | |||
| @@ -86,7 +87,7 @@ | |||
| "Please scan warehouse qr code.": "請掃描倉庫 QR 碼。", | |||
| "Reject": "拒絕", | |||
| "submit": "提交", | |||
| "submit": "確認提交", | |||
| "print": "列印", | |||
| "bind": "綁定", | |||
| @@ -101,7 +102,7 @@ | |||
| "Consolidated Pick Orders": "合併提料單", | |||
| "Pick Order No.": "提料單編號", | |||
| "Pick Order Date": "提料單日期", | |||
| "Pick Order Status": "提料單狀態", | |||
| "Pick Order Status": "提貨狀態", | |||
| "Pick Order Type": "提料單類型", | |||
| "Consolidated Code": "合併編號", | |||
| "type": "類型", | |||
| @@ -111,7 +112,7 @@ | |||
| "Target Date From": "目標日期從", | |||
| "Target Date To": "目標日期到", | |||
| "Consolidate": "合併", | |||
| "Stock Unit": "庫存單位", | |||
| "create": "新增", | |||
| "detail": "詳情", | |||
| "Pick Order Detail": "提料單詳情", | |||
| @@ -130,20 +131,44 @@ | |||
| "lot change": "批次變更", | |||
| "checkout": "出庫", | |||
| "Search Items": "搜尋貨品", | |||
| "Search Results": "搜尋結果", | |||
| "Search Results": "可選擇貨品", | |||
| "Second Search Results": "第二搜尋結果", | |||
| "Second Search Items": "第二搜尋項目", | |||
| "Second Search": "第二搜尋", | |||
| "Item": "貨品", | |||
| "Order Quantity": "要求數量", | |||
| "Current Stock": "現時庫存", | |||
| "Order Quantity": "貨品需求數量", | |||
| "Current Stock": "現時可用庫存", | |||
| "Selected": "已選擇", | |||
| "Select Items": "選擇貨品", | |||
| "Assign": "分派提料單", | |||
| "Release": "放單", | |||
| "Pick Execution": "進行提料", | |||
| "Create Pick Order": "建立貨品提料單", | |||
| "Consumable": "食材", | |||
| "Material": "材料", | |||
| "Job Order": "工單" | |||
| } | |||
| "Consumable": "消耗品", | |||
| "Material": "食材", | |||
| "Job Order": "工單", | |||
| "End Product": "成品", | |||
| "Lot Expiry Date": "批號到期日", | |||
| "Lot Location": "批號位置", | |||
| "Available Lot": "批號可用提料數量", | |||
| "Lot Required Pick Qty": "批號所需提料數量", | |||
| "Lot Actual Pick Qty": "批號實際提料數量", | |||
| "Lot#": "批號", | |||
| "Submit": "提交", | |||
| "Created Items": "已建立貨品", | |||
| "Create New Group": "建立新分組", | |||
| "Group": "分組", | |||
| "Qty Already Picked": "已提料數量", | |||
| "Select Job Order Items": "選擇工單貨品", | |||
| "failedQty": "不合格項目數量", | |||
| "remarks": "備註", | |||
| "Qc items": "QC 項目", | |||
| "qcItem": "QC 項目", | |||
| "QC Info": "QC 資訊", | |||
| "qcResult": "QC 結果", | |||
| "acceptQty": "接受數量", | |||
| "Escalation History": "上報歷史", | |||
| "Group Name": "分組名稱", | |||
| "Job Order Code": "工單編號" | |||
| } | |||