@@ -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": "工單編號" | |||||
} |