| @@ -119,6 +119,98 @@ export interface CurrentInventoryItemInfo { | |||||
| requiredQty: number; | 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) => { | export const fetchPickOrderDetails = cache(async (ids: string) => { | ||||
| return serverFetchJson<GetPickOrderInfoResponse>( | return serverFetchJson<GetPickOrderInfoResponse>( | ||||
| @@ -16,7 +16,25 @@ export interface QcResult { | |||||
| stockOutLineId?: number; | stockOutLineId?: number; | ||||
| failQty: 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) => { | export const fetchQcItemCheck = cache(async (itemId?: number) => { | ||||
| let url = `${BASE_API_URL}/qcCheck`; | let url = `${BASE_API_URL}/qcCheck`; | ||||
| if (itemId) url += `/${itemId}`; | 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"; | } from "@/app/utils/fetchUtil"; | ||||
| import { revalidateTag } from "next/cache"; | import { revalidateTag } from "next/cache"; | ||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| import { CreateItemResponse } from "../../utils"; | |||||
| import { CreateItemResponse, RecordsRes } from "../../utils"; | |||||
| import { ItemQc, ItemsResult } from "."; | import { ItemQc, ItemsResult } from "."; | ||||
| import { QcChecksInputs } from "../qcCheck/actions"; | import { QcChecksInputs } from "../qcCheck/actions"; | ||||
| import { cache } from "react"; | import { cache } from "react"; | ||||
| // export type TypeInputs = { | // export type TypeInputs = { | ||||
| // id: number; | // id: number; | ||||
| // name: string | // name: string | ||||
| @@ -56,6 +57,7 @@ export interface ItemCombo { | |||||
| label: string, | label: string, | ||||
| uomId: number, | uomId: number, | ||||
| uom: string, | uom: string, | ||||
| uomDesc: string, | |||||
| group?: string, | group?: string, | ||||
| currentStockBalance?: number, | currentStockBalance?: number, | ||||
| } | } | ||||
| @@ -65,3 +67,25 @@ export const fetchAllItemsInClient = cache(async () => { | |||||
| next: { tags: ["items"] }, | 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; | name: string; | ||||
| } | } | ||||
| export interface NewNameList { | |||||
| id: number; | |||||
| name: string; | |||||
| title: string; | |||||
| department: string; | |||||
| } | |||||
| export const fetchUserDetails = cache(async (id: number) => { | export const fetchUserDetails = cache(async (id: number) => { | ||||
| return serverFetchJson<UserDetail>(`${BASE_API_URL}/user/${id}`, { | return serverFetchJson<UserDetail>(`${BASE_API_URL}/user/${id}`, { | ||||
| next: { tags: ["user"] }, | 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) => { | export const editUser = async (id: number, data: UserInputs) => { | ||||
| const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { | const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { | ||||
| method: "PUT", | method: "PUT", | ||||
| @@ -9,9 +9,6 @@ import { | |||||
| Modal, | Modal, | ||||
| TextField, | TextField, | ||||
| Typography, | Typography, | ||||
| Accordion, | |||||
| AccordionSummary, | |||||
| AccordionDetails, | |||||
| Table, | Table, | ||||
| TableBody, | TableBody, | ||||
| TableCell, | TableCell, | ||||
| @@ -19,36 +16,27 @@ import { | |||||
| TableHead, | TableHead, | ||||
| TableRow, | TableRow, | ||||
| Paper, | Paper, | ||||
| Checkbox, | |||||
| TablePagination, | |||||
| Alert, | |||||
| AlertTitle, | |||||
| } from "@mui/material"; | } from "@mui/material"; | ||||
| import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; | |||||
| 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 SearchResults, { Column } from "../SearchResults/SearchResults"; | |||||
| import { | import { | ||||
| PickOrderResult, | |||||
| } from "@/app/api/pickOrder"; | |||||
| import { | |||||
| assignPickOrder, | |||||
| fetchPickOrderClient, | |||||
| newassignPickOrder, | |||||
| AssignPickOrderInputs, | |||||
| fetchPickOrderWithStockClient, | fetchPickOrderWithStockClient, | ||||
| releasePickOrder, | |||||
| ReleasePickOrderInputs, | |||||
| GetPickOrderInfo, | |||||
| GetPickOrderLineInfo, | |||||
| } from "@/app/api/pickOrder/actions"; | } 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 useUploadContext from "../UploadProvider/useUploadContext"; | ||||
| import dayjs from "dayjs"; | 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 { flatten, intersectionWith, sortBy, uniqBy } from "lodash"; | |||||
| import { arrayToDayjs } from "@/app/utils/formatUtil"; | |||||
| import { fetchPickOrderItemsByPageClient } from "@/app/api/settings/item/actions"; | |||||
| dayjs.extend(arraySupport); | dayjs.extend(arraySupport); | ||||
| @@ -56,6 +44,56 @@ interface Props { | |||||
| filterArgs: Record<string, any>; | 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 = { | const style = { | ||||
| position: "absolute", | position: "absolute", | ||||
| top: "50%", | top: "50%", | ||||
| @@ -71,73 +109,85 @@ const style = { | |||||
| const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | ||||
| const { t } = useTranslation("pickOrder"); | const { t } = useTranslation("pickOrder"); | ||||
| const { setIsUploading } = useUploadContext(); | 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({ | const [pagingController, setPagingController] = useState({ | ||||
| pageNum: 0, | |||||
| pageNum: 1, | |||||
| pageSize: 10, | pageSize: 10, | ||||
| }); | }); | ||||
| const [totalCountPickOrders, setTotalCountPickOrders] = useState<number>(); | |||||
| // State for Assign & Release Modal | |||||
| const [totalCountItems, setTotalCountItems] = useState<number>(); | |||||
| const [modalOpen, setModalOpen] = useState(false); | 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 [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; | 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( | 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"), | label: t("Target Date From"), | ||||
| @@ -146,14 +196,14 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| type: "dateRange", | type: "dateRange", | ||||
| }, | }, | ||||
| { | { | ||||
| label: t("Status"), | |||||
| label: t("Pick Order Status"), | |||||
| paramName: "status", | paramName: "status", | ||||
| type: "autocomplete", | type: "autocomplete", | ||||
| options: sortBy( | options: sortBy( | ||||
| uniqBy( | uniqBy( | ||||
| originalPickOrderData.map((po) => ({ | |||||
| value: po.status, | |||||
| label: t(upperFirst(po.status)), | |||||
| originalPickOrderData.map((pickOrder) => ({ | |||||
| value: pickOrder.status, | |||||
| label: t(upperFirst(pickOrder.status)), | |||||
| })), | })), | ||||
| "value", | "value", | ||||
| ), | ), | ||||
| @@ -164,103 +214,144 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| [originalPickOrderData, t], | [originalPickOrderData, t], | ||||
| ); | ); | ||||
| // Add search handler | |||||
| // Update search function to work with pick order data | |||||
| const handleSearch = useCallback((query: Record<string, any>) => { | const handleSearch = useCallback((query: Record<string, any>) => { | ||||
| console.log("AssignAndRelease search triggered with query:", query); | |||||
| setSearchQuery({ ...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 || | 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 || | const statusMatch = !query.status || | ||||
| query.status.toLowerCase() === "all" || | 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]); | }, [originalPickOrderData]); | ||||
| // Add reset handler | |||||
| const handleReset = useCallback(() => { | const handleReset = useCallback(() => { | ||||
| setSearchQuery({}); | setSearchQuery({}); | ||||
| // Reset to original data | |||||
| setFilteredPickOrder(originalPickOrderData); | |||||
| setFilteredPickOrders(originalPickOrderData); | |||||
| setTimeout(() => { | |||||
| setSearchQuery({}); | |||||
| }, 0); | |||||
| }, [originalPickOrderData]); | }, [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); | setIsUploading(true); | ||||
| try { | 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); | 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) { | } catch (error) { | ||||
| console.error("Error in assign and release:", error); | |||||
| console.error("Error in assign:", error); | |||||
| } finally { | } finally { | ||||
| setIsUploading(false); | setIsUploading(false); | ||||
| } | } | ||||
| }, [selectedRows, setIsUploading, fetchNewPagePickOrder, pagingController, filterArgs]); | |||||
| }, [selectedPickOrderIds, setIsUploading, fetchNewPageItems, pagingController, filterArgs]); | |||||
| // Open assign & release modal | |||||
| const openAssignModal = useCallback(() => { | const openAssignModal = useCallback(() => { | ||||
| setModalOpen(true); | setModalOpen(true); | ||||
| // Reset form | |||||
| formProps.reset(); | formProps.reset(); | ||||
| }, [formProps]); | }, [formProps]); | ||||
| // Load data | |||||
| // Component mount effect | |||||
| useEffect(() => { | 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(() => { | useEffect(() => { | ||||
| const loadUsernameList = async () => { | const loadUsernameList = async () => { | ||||
| try { | try { | ||||
| const res = await fetchNameList(); | |||||
| const res = await fetchNewNameList(); | |||||
| if (res) { | if (res) { | ||||
| setUsernameList(res); | setUsernameList(res); | ||||
| } | } | ||||
| @@ -271,143 +362,141 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| loadUsernameList(); | 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 ( | return ( | ||||
| <> | <> | ||||
| {/* Search Box */} | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={handleSearch} | |||||
| onReset={handleReset} | |||||
| /> | |||||
| {/* Pick Orders View */} | |||||
| <SearchBox criteria={searchCriteria} onSearch={handleSearch} onReset={handleReset} /> | |||||
| <Grid container rowGap={1}> | <Grid container rowGap={1}> | ||||
| {/* Remove the button from here */} | |||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| {isLoadingPickOrders ? ( | |||||
| {isLoadingItems ? ( | |||||
| <CircularProgress size={40} /> | <CircularProgress size={40} /> | ||||
| ) : ( | ) : ( | ||||
| <SearchResults<GetPickOrderInfo> | |||||
| items={filteredPickOrder} | |||||
| columns={pickOrderColumns} | |||||
| pagingController={pagingController} | |||||
| setPagingController={setPagingController} | |||||
| totalCount={totalCountPickOrders} | |||||
| checkboxIds={selectedRows!} | |||||
| setCheckboxIds={setSelectedRows} | |||||
| /> | |||||
| <CustomPickOrderTable /> | |||||
| )} | )} | ||||
| </Grid> | </Grid> | ||||
| {/* Add the button below the table */} | |||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <Box sx={{ display: 'flex', justifyContent: 'flex-start', mt: 2 }}> | |||||
| <Box sx={{ display: "flex", justifyContent: "flex-start", mt: 2 }}> | |||||
| <Button | <Button | ||||
| disabled={selectedRows.length < 1} | |||||
| disabled={selectedPickOrderIds.length < 1} | |||||
| variant="outlined" | variant="outlined" | ||||
| onClick={openAssignModal} | onClick={openAssignModal} | ||||
| > | > | ||||
| {t("Assign & Release")} | |||||
| {t("Assign")} | |||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| {/* Assign & Release Modal */} | |||||
| {modalOpen ? ( | {modalOpen ? ( | ||||
| <Modal | <Modal | ||||
| open={modalOpen} | open={modalOpen} | ||||
| @@ -419,25 +508,37 @@ const AssignAndRelease: React.FC<Props> = ({ filterArgs }) => { | |||||
| <Grid container rowGap={2}> | <Grid container rowGap={2}> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <Typography variant="h6" component="h2"> | <Typography variant="h6" component="h2"> | ||||
| {t("assign & Release Pick Orders")} | |||||
| {t("Assign Pick Orders")} | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <Typography variant="body1" color="text.secondary"> | <Typography variant="body1" color="text.secondary"> | ||||
| {t("Selected Pick Orders")}: {selectedRows.length} | |||||
| {t("Selected Pick Orders")}: {selectedPickOrderIds.length} | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <FormProvider {...formProps}> | |||||
| <FormProvider {...formProps}> | |||||
| <form onSubmit={formProps.handleSubmit(handleAssignAndRelease)}> | <form onSubmit={formProps.handleSubmit(handleAssignAndRelease)}> | ||||
| <Grid container spacing={2}> | <Grid container spacing={2}> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| <FormControl fullWidth> | <FormControl fullWidth> | ||||
| <Autocomplete | <Autocomplete | ||||
| options={usernameList} | 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) => { | onChange={(_, value) => { | ||||
| formProps.setValue("assignTo", value?.id || 0); | formProps.setValue("assignTo", value?.id || 0); | ||||
| }} | }} | ||||
| @@ -455,23 +556,16 @@ 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 and release them immediately.")} | |||||
| {t("This action will assign the selected pick orders.")} | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12}> | <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")} | {t("Cancel")} | ||||
| </Button> | </Button> | ||||
| <Button | |||||
| type="submit" | |||||
| variant="contained" | |||||
| color="primary" | |||||
| > | |||||
| {t("Assign & Release")} | |||||
| <Button type="submit" variant="contained" color="primary"> | |||||
| {t("Assign")} | |||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| </Grid> | </Grid> | ||||
| @@ -25,6 +25,7 @@ import AssignAndRelease from "./AssignAndRelease"; | |||||
| import AssignTo from "./assignTo"; | import AssignTo from "./assignTo"; | ||||
| import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | import { fetchAllItemsInClient, ItemCombo } from "@/app/api/settings/item/actions"; | ||||
| import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; | import { fetchPickOrderClient } from "@/app/api/pickOrder/actions"; | ||||
| import Jobcreatitem from "./Jobcreatitem"; | |||||
| interface Props { | interface Props { | ||||
| pickOrders: PickOrderResult[]; | pickOrders: PickOrderResult[]; | ||||
| @@ -224,6 +225,12 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => { | |||||
| } | } | ||||
| }, [isOpenCreateModal]) | }, [isOpenCreateModal]) | ||||
| // 添加处理提料单创建成功的函数 | |||||
| const handlePickOrderCreated = useCallback(() => { | |||||
| // 切换到 Assign & Release 标签页 (tabIndex = 1) | |||||
| setTabIndex(1); | |||||
| }, []); | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Stack | <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("Assign")} iconPosition="end" /> | ||||
| <Tab label={t("Release")} iconPosition="end" /> | <Tab label={t("Release")} iconPosition="end" /> | ||||
| <Tab label={t("Pick Execution")} 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> | </Tabs> | ||||
| {tabIndex === 4 && ( | |||||
| {/*{tabIndex === 4 && ( | |||||
| <PickOrders | <PickOrders | ||||
| filteredPickOrders={filteredPickOrders} | filteredPickOrders={filteredPickOrders} | ||||
| filterArgs={filterArgs} | 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, | ModalProps, | ||||
| Stack, | Stack, | ||||
| Typography, | Typography, | ||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Paper, | |||||
| TextField, | TextField, | ||||
| Radio, | Radio, | ||||
| RadioGroup, | RadioGroup, | ||||
| FormControlLabel, | FormControlLabel, | ||||
| FormControl, | FormControl, | ||||
| Tab, | |||||
| Tabs, | |||||
| TabsProps, | |||||
| Paper, | |||||
| } from "@mui/material"; | } 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 { 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 { 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 = { | const style = { | ||||
| position: "absolute", | position: "absolute", | ||||
| @@ -40,10 +49,11 @@ const style = { | |||||
| px: 5, | px: 5, | ||||
| pb: 10, | pb: 10, | ||||
| display: "block", | 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"> { | interface CommonProps extends Omit<ModalProps, "children"> { | ||||
| itemDetail: GetPickOrderLineInfo & { | itemDetail: GetPickOrderLineInfo & { | ||||
| pickOrderCode: string; | pickOrderCode: string; | ||||
| @@ -67,31 +77,47 @@ interface Props extends CommonProps { | |||||
| pickOrderCode: string; | pickOrderCode: string; | ||||
| qcResult?: PurchaseQcResult[] | 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, | open, | ||||
| onClose, | onClose, | ||||
| itemDetail, | itemDetail, | ||||
| setItemDetail, | setItemDetail, | ||||
| qc, | qc, | ||||
| warehouse, | warehouse, | ||||
| qcItems, | |||||
| setQcItems, | |||||
| }) => { | }) => { | ||||
| console.log(warehouse); | |||||
| const { | const { | ||||
| t, | t, | ||||
| i18n: { language }, | i18n: { language }, | ||||
| } = useTranslation("pickOrder"); | } = 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>({ | const formProps = useForm<any>({ | ||||
| defaultValues: { | defaultValues: { | ||||
| qcAccept: true, | |||||
| acceptQty: itemDetail.requiredQty ?? 0, | |||||
| qcDecision: "1", // Default to accept | |||||
| ...itemDetail, | ...itemDetail, | ||||
| }, | }, | ||||
| }); | }); | ||||
| const { control, register, formState: { errors }, watch, setValue } = formProps; | |||||
| const qcDecision = watch("qcDecision"); | |||||
| const accQty = watch("acceptQty"); | |||||
| const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | const closeHandler = useCallback<NonNullable<ModalProps["onClose"]>>( | ||||
| (...args) => { | (...args) => { | ||||
| onClose?.(...args); | onClose?.(...args); | ||||
| @@ -99,228 +125,368 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||||
| [onClose], | [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>>( | const onSubmitQc = useCallback<SubmitHandler<any>>( | ||||
| async (data, event) => { | 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 ( | return ( | ||||
| <> | <> | ||||
| <FormProvider {...formProps}> | <FormProvider {...formProps}> | ||||
| <Modal open={open} onClose={closeHandler}> | <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}> | <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> | </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}> | <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> | </Grid> | ||||
| {/* buttons */} | |||||
| {qcDecision == 3 && ( | |||||
| <Grid item xs={12}> | |||||
| <EscalationComponent | |||||
| forSupervisor={false} | |||||
| isCollapsed={isCollapsed} | |||||
| setIsCollapsed={setIsCollapsed} | |||||
| /> | |||||
| </Grid> | |||||
| )} | |||||
| <Grid item xs={12} sx={{ mt: 2 }}> | <Grid item xs={12} sx={{ mt: 2 }}> | ||||
| <Stack direction="row" justifyContent="flex-start" gap={1}> | <Stack direction="row" justifyContent="flex-start" gap={1}> | ||||
| <Button | <Button | ||||
| variant="contained" | variant="contained" | ||||
| onClick={formProps.handleSubmit(onSubmitQc)} | 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> | ||||
| <Button | <Button | ||||
| variant="contained" | |||||
| variant="outlined" | |||||
| onClick={() => { | onClick={() => { | ||||
| console.log("Reject and pick another lot"); | |||||
| onClose?.(); | |||||
| closeHandler?.({}, 'escapeKeyDown'); | |||||
| }} | }} | ||||
| > | > | ||||
| Reject and Pick Another Lot | |||||
| Cancel | |||||
| </Button> | </Button> | ||||
| </Stack> | </Stack> | ||||
| </Grid> | </Grid> | ||||
| @@ -332,4 +498,4 @@ const PickQcStockInModalVer2: React.FC<Props> = ({ | |||||
| ); | ); | ||||
| }; | }; | ||||
| export default PickQcStockInModalVer2; | |||||
| export default PickQcStockInModalVer3; | |||||
| @@ -4,93 +4,381 @@ import { | |||||
| Box, | Box, | ||||
| Button, | Button, | ||||
| CircularProgress, | CircularProgress, | ||||
| FormControl, | |||||
| Grid, | Grid, | ||||
| Stack, | |||||
| Modal, | |||||
| TextField, | TextField, | ||||
| Typography, | Typography, | ||||
| Table, | |||||
| TableBody, | |||||
| TableCell, | |||||
| TableContainer, | |||||
| TableHead, | |||||
| TableRow, | |||||
| Paper, | |||||
| Checkbox, | |||||
| TablePagination, | |||||
| } 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 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 { 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 { | interface Props { | ||||
| filterArgs: Record<string, any>; | filterArgs: Record<string, any>; | ||||
| } | } | ||||
| interface AssignmentData { | |||||
| // 使用 fetchPickOrderItemsByPageClient 返回的数据结构 | |||||
| interface ItemRow { | |||||
| id: string; | 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; | 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 AssignTo: React.FC<Props> = ({ filterArgs }) => { | ||||
| const { t } = useTranslation("pickOrder"); | 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({ | const [pagingController, setPagingController] = useState({ | ||||
| pageNum: 1, | 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 [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 { | 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) { | } catch (error) { | ||||
| console.error("Error fetching assignment data:", error); | |||||
| console.error("Error in assign:", error); | |||||
| } finally { | } 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(() => { | useEffect(() => { | ||||
| const loadUsernameList = async () => { | const loadUsernameList = async () => { | ||||
| try { | try { | ||||
| const res = await fetchNameList(); | const res = await fetchNameList(); | ||||
| if (res) { | if (res) { | ||||
| console.log("Loaded username list:", res); | |||||
| setUsernameList(res); | setUsernameList(res); | ||||
| } | } | ||||
| } catch (error) { | } catch (error) { | ||||
| @@ -100,134 +388,157 @@ const AssignTo: React.FC<Props> = ({ filterArgs }) => { | |||||
| loadUsernameList(); | 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 ( | 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> | ||||
| <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> | </Grid> | ||||
| </Stack> | |||||
| </> | |||||
| ); | ); | ||||
| }; | }; | ||||
| export default AssignTo; | |||||
| export default AssignTo; | |||||
| @@ -7,9 +7,10 @@ | |||||
| "Details": "詳情", | "Details": "詳情", | ||||
| "Supplier": "供應商", | "Supplier": "供應商", | ||||
| "Status": "來貨狀態", | "Status": "來貨狀態", | ||||
| "Release Pick Orders": "放單", | |||||
| "Escalated": "上報狀態", | "Escalated": "上報狀態", | ||||
| "NotEscalated": "無上報", | "NotEscalated": "無上報", | ||||
| "Assigned To": "已分配", | |||||
| "Do you want to start?": "確定開始嗎?", | "Do you want to start?": "確定開始嗎?", | ||||
| "Start": "開始", | "Start": "開始", | ||||
| "Start Success": "開始成功", | "Start Success": "開始成功", | ||||
| @@ -86,7 +87,7 @@ | |||||
| "Please scan warehouse qr code.": "請掃描倉庫 QR 碼。", | "Please scan warehouse qr code.": "請掃描倉庫 QR 碼。", | ||||
| "Reject": "拒絕", | "Reject": "拒絕", | ||||
| "submit": "提交", | |||||
| "submit": "確認提交", | |||||
| "print": "列印", | "print": "列印", | ||||
| "bind": "綁定", | "bind": "綁定", | ||||
| @@ -101,7 +102,7 @@ | |||||
| "Consolidated Pick Orders": "合併提料單", | "Consolidated Pick Orders": "合併提料單", | ||||
| "Pick Order No.": "提料單編號", | "Pick Order No.": "提料單編號", | ||||
| "Pick Order Date": "提料單日期", | "Pick Order Date": "提料單日期", | ||||
| "Pick Order Status": "提料單狀態", | |||||
| "Pick Order Status": "提貨狀態", | |||||
| "Pick Order Type": "提料單類型", | "Pick Order Type": "提料單類型", | ||||
| "Consolidated Code": "合併編號", | "Consolidated Code": "合併編號", | ||||
| "type": "類型", | "type": "類型", | ||||
| @@ -111,7 +112,7 @@ | |||||
| "Target Date From": "目標日期從", | "Target Date From": "目標日期從", | ||||
| "Target Date To": "目標日期到", | "Target Date To": "目標日期到", | ||||
| "Consolidate": "合併", | "Consolidate": "合併", | ||||
| "Stock Unit": "庫存單位", | |||||
| "create": "新增", | "create": "新增", | ||||
| "detail": "詳情", | "detail": "詳情", | ||||
| "Pick Order Detail": "提料單詳情", | "Pick Order Detail": "提料單詳情", | ||||
| @@ -130,20 +131,44 @@ | |||||
| "lot change": "批次變更", | "lot change": "批次變更", | ||||
| "checkout": "出庫", | "checkout": "出庫", | ||||
| "Search Items": "搜尋貨品", | "Search Items": "搜尋貨品", | ||||
| "Search Results": "搜尋結果", | |||||
| "Search Results": "可選擇貨品", | |||||
| "Second Search Results": "第二搜尋結果", | "Second Search Results": "第二搜尋結果", | ||||
| "Second Search Items": "第二搜尋項目", | "Second Search Items": "第二搜尋項目", | ||||
| "Second Search": "第二搜尋", | "Second Search": "第二搜尋", | ||||
| "Item": "貨品", | "Item": "貨品", | ||||
| "Order Quantity": "要求數量", | |||||
| "Current Stock": "現時庫存", | |||||
| "Order Quantity": "貨品需求數量", | |||||
| "Current Stock": "現時可用庫存", | |||||
| "Selected": "已選擇", | "Selected": "已選擇", | ||||
| "Select Items": "選擇貨品", | "Select Items": "選擇貨品", | ||||
| "Assign": "分派提料單", | "Assign": "分派提料單", | ||||
| "Release": "放單", | "Release": "放單", | ||||
| "Pick Execution": "進行提料", | "Pick Execution": "進行提料", | ||||
| "Create Pick Order": "建立貨品提料單", | "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": "工單編號" | |||||
| } | |||||