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