Browse Source

updated

master
CANCERYS\kw093 1 day ago
parent
commit
2313e36d79
13 changed files with 4295 additions and 762 deletions
  1. +27
    -2
      src/app/api/inventory/actions.ts
  2. +53
    -0
      src/app/api/pickOrder/actions.ts
  3. +160
    -137
      src/components/PickOrderSearch/AssignAndRelease.tsx
  4. +209
    -0
      src/components/PickOrderSearch/CreatedItemsTable.tsx
  5. +327
    -0
      src/components/PickOrderSearch/LotTable.tsx
  6. +316
    -232
      src/components/PickOrderSearch/PickExecution.tsx
  7. +142
    -45
      src/components/PickOrderSearch/PickQcStockInModalVer3.tsx
  8. +242
    -0
      src/components/PickOrderSearch/SearchResultsTable.tsx
  9. +85
    -0
      src/components/PickOrderSearch/VerticalSearchBox.tsx
  10. +184
    -217
      src/components/PickOrderSearch/assignTo.tsx
  11. +2234
    -0
      src/components/PickOrderSearch/newcreatitem copy.tsx
  12. +296
    -111
      src/components/PickOrderSearch/newcreatitem.tsx
  13. +20
    -18
      src/i18n/zh/pickOrder.json

+ 27
- 2
src/app/api/inventory/actions.ts View File

@@ -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)


+ 53
- 0
src/app/api/pickOrder/actions.ts View File

@@ -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>(


+ 160
- 137
src/components/PickOrderSearch/AssignAndRelease.tsx View File

@@ -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}>


+ 209
- 0
src/components/PickOrderSearch/CreatedItemsTable.tsx View File

@@ -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;

+ 327
- 0
src/components/PickOrderSearch/LotTable.tsx View File

@@ -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;

+ 316
- 232
src/components/PickOrderSearch/PickExecution.tsx View File

@@ -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>


+ 142
- 45
src/components/PickOrderSearch/PickQcStockInModalVer3.tsx View File

@@ -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>
)}


+ 242
- 0
src/components/PickOrderSearch/SearchResultsTable.tsx View File

@@ -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;

+ 85
- 0
src/components/PickOrderSearch/VerticalSearchBox.tsx View File

@@ -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;

+ 184
- 217
src/components/PickOrderSearch/assignTo.tsx View File

@@ -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}>


+ 2234
- 0
src/components/PickOrderSearch/newcreatitem copy.tsx
File diff suppressed because it is too large
View File


+ 296
- 111
src/components/PickOrderSearch/newcreatitem.tsx View File

@@ -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 }}>


+ 20
- 18
src/i18n/zh/pickOrder.json View File

@@ -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掃描"

}

Loading…
Cancel
Save