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