Browse Source

update

master
CANCERYS\kw093 1 month ago
parent
commit
7e827c5192
35 changed files with 1834 additions and 785 deletions
  1. +4
    -4
      check-translations.js
  2. +8
    -9
      src/app/(main)/production/page.tsx
  3. +133
    -3
      src/app/api/jo/actions.ts
  4. +24
    -22
      src/app/api/pickOrder/actions.ts
  5. +12
    -12
      src/components/DoDetail/DoDetail.tsx
  6. +3
    -3
      src/components/FinishedGoodSearch/CombinedLotTable.tsx
  7. +6
    -5
      src/components/FinishedGoodSearch/FGPickOrderCard.tsx
  8. +6
    -6
      src/components/FinishedGoodSearch/FGPickOrderInfoCard.tsx
  9. +25
    -16
      src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx
  10. +6
    -6
      src/components/FinishedGoodSearch/FinishedGoodSearch.tsx
  11. +53
    -53
      src/components/FinishedGoodSearch/GoodPickExecution.tsx
  12. +13
    -13
      src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx
  13. +30
    -30
      src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx
  14. +131
    -131
      src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx
  15. +38
    -38
      src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx
  16. +2
    -2
      src/components/FinishedGoodSearch/newcreatitem.tsx
  17. +3
    -3
      src/components/Jodetail/CombinedLotTable.tsx
  18. +30
    -30
      src/components/Jodetail/FInishedJobOrderRecord.tsx
  19. +90
    -90
      src/components/Jodetail/JobPickExecution.tsx
  20. +20
    -20
      src/components/Jodetail/JobPickExecutionForm.tsx
  21. +73
    -73
      src/components/Jodetail/JobPickExecutionsecondscan.tsx
  22. +21
    -21
      src/components/Jodetail/JobmatchForm.tsx
  23. +10
    -10
      src/components/Jodetail/JodetailSearch.tsx
  24. +32
    -32
      src/components/Jodetail/completeJobOrderRecord.tsx
  25. +66
    -66
      src/components/PickOrderSearch/LotTable.tsx
  26. +20
    -20
      src/components/PickOrderSearch/PickExecution.tsx
  27. +10
    -10
      src/components/PickOrderSearch/PickExecutionForm.tsx
  28. +3
    -3
      src/components/PickOrderSearch/PickOrderDetailsTable.tsx
  29. +38
    -38
      src/components/PickOrderSearch/PickQcStockInModalVer3.tsx
  30. +2
    -2
      src/components/PickOrderSearch/newcreatitem.tsx
  31. +6
    -6
      src/components/ProductionProcess/MachineScanner.tsx
  32. +8
    -8
      src/components/ProductionProcess/OperatorScanner.tsx
  33. +656
    -0
      src/components/ProductionProcess/ProductionProcessDetail.tsx
  34. +185
    -0
      src/components/ProductionProcess/ProductionProcessList.tsx
  35. +67
    -0
      src/components/ProductionProcess/ProductionProcessPage.tsx

+ 4
- 4
check-translations.js View File

@@ -11,7 +11,7 @@ function checkMissingTranslations(sourceFile, jsonFile) {
// 读取翻译 JSON 文件
const translations = JSON.parse(fs.readFileSync(jsonFile, 'utf-8'));
// 只匹配 t('...') 和 t("...") 和 t(`...`),不包含模板变量
// 只匹配 t('...') 和 t("...") 和 t(`...`),不包含模板变量
const tRegex = /\bt\(["`']([^"`'${}]+)["`']\)/g;
const matches = [...sourceCode.matchAll(tRegex)];
@@ -86,7 +86,7 @@ if (args.length === 0) {
console.log(' node check-translations.js src/components/Jodetail/JodetailSearch.tsx src/i18n/zh/jo.json');
console.log(' node check-translations.js --dir src/components/Jodetail src/i18n/zh/jo.json');
console.log('\n注意:');
console.log(' 只检查 t("key") 调用');
console.log(' 只检查 t("key") 调用');
console.log(' ❌ 忽略 alert(), console.log() 等普通字符串');
console.log(' ❌ 忽略模板字符串中的 ${} 变量部分');
process.exit(0);
@@ -103,7 +103,7 @@ if (args[0] === '--dir') {
const { results, totalMissing } = checkDirectory(directory, jsonFile);
if (Object.keys(results).length === 0) {
console.log(' 太棒了!没有发现缺失的翻译键!');
console.log(' 太棒了!没有发现缺失的翻译键!');
} else {
console.log(`⚠️ 发现 ${Object.keys(results).length} 个文件有缺失的翻译键\n`);
@@ -153,7 +153,7 @@ if (args[0] === '--dir') {
});
console.log('─'.repeat(60));
} else {
console.log('\n 太棒了!所有使用的翻译键都已定义!');
console.log('\n 太棒了!所有使用的翻译键都已定义!');
}
if (result.unusedKeys.length > 0 && result.unusedKeys.length <= 20) {


+ 8
- 9
src/app/(main)/production/page.tsx View File

@@ -1,4 +1,4 @@
import ProductionProcess from "../../../components/ProductionProcess";
import ProductionProcessPage from "../../../components/ProductionProcess/ProductionProcessPage";
import { getServerI18n } from "../../../i18n";

import Add from "@mui/icons-material/Add";
@@ -15,7 +15,6 @@ export const metadata: Metadata = {

const production: React.FC = async () => {
const { t } = await getServerI18n("claims");
// preloadClaims();

return (
<>
@@ -26,22 +25,22 @@ const production: React.FC = async () => {
rowGap={2}
>
<Typography variant="h4" marginInlineEnd={2}>
{t("Production")}
{t("Production Process")}
</Typography>
<Button
{/* Optional: Remove or modify create button, because creation is done via API automatically */}
{/* <Button
variant="contained"
startIcon={<Add />}
LinkComponent={Link}
href="/production/create"
>
{t("Create Claim")}
</Button>
{t("Create Process")}
</Button> */}
</Stack>
{/* <Suspense fallback={<ClaimSearch.Loading />}> */}
<ProductionProcess />
{/* </Suspense> */}
<ProductionProcessPage /> {/* Use new component */}
</>
);
};


export default production;

+ 133
- 3
src/app/api/jo/actions.ts View File

@@ -129,13 +129,13 @@ export const recordSecondScanIssue = cache(async (
itemId: number,
data: {
qty: number; // verified qty (actual pick qty)
missQty?: number; // 添加:miss qty
badItemQty?: number; // 添加:bad item qty
missQty?: number; // 添加:miss qty
badItemQty?: number; // 添加:bad item qty
isMissing: boolean;
isBad: boolean;
reason: string;
createdBy: number;
type?: string; // type 也应该是可选的
type?: string; // type 也应该是可选的
}
) => {
@@ -188,6 +188,136 @@ export interface ProductProcessWithLinesResponse {
date: string;
lines: ProductProcessLineResponse[];
}
export interface UpdateProductProcessLineQtyRequest {
productProcessLineId: number;
outputFromProcessQty: number;
outputFromProcessUom: string;
defectQty: number;
defectUom: string;
scrapQty: number;
scrapUom: string;
}
export interface UpdateProductProcessLineQtyResponse {
id: number;
outputFromProcessQty: number;
outputFromProcessUom: string;
defectQty: number;
defectUom: string;
scrapQty: number;
scrapUom: string;
byproductName: string;
byproductQty: number;
byproductUom: string;
}
export interface AllProductProcessResponse {
id: number;
productProcessCode: string;
status: string;
startTime?: string;
endTime?: string;
date: string;
bomId?: number;
}
export interface AllJoborderProductProcessInfoResponse {
id: number;
productProcessCode: string;
status: string;
startTime?: string;
endTime?: string;
date: string;
bomId?: number;
itemName: string;
jobOrderId: number;
jobOrderCode: string;
productProcessLineCount: number;
FinishedProductProcessLineCount: number;
lines: ProductProcessInfoResponse[];
}
export interface ProductProcessInfoResponse {
id: number;
operatorId?: number;
operatorName?: string;
equipmentId?: number;
equipmentName?: string;
startTime?: string;
endTime?: string;
status: string;
}
export interface ProductProcessLineQrscanUpadteRequest {
lineId: number;
operatorId?: number;
equipmentId?: number;
}
export interface ProductProcessLineDetailResponse {
id: number,
productProcessId: number,
bomProcessId: number,
operatorId: number,
equipmentType: string,
operatorName: string,
handlerId: number,
seqNo: number,
name: string,
description: string,
equipment: string,
startTime: string,
endTime: string,
defectQty: number,
defectUom: string,
scrapQty: number,
scrapUom: string,
byproductId: number,
byproductName: string,
byproductQty: number,
byproductUom: string | undefined,
}
export const fetchProductProcessLineDetailByJoid = cache(async (joid: number) => {
return serverFetchJson<ProductProcessLineDetailResponse>(
`${BASE_API_URL}/product-process/demo/joid/${joid}`,
{
method: "GET",
}
);
});

// /product-process/Demo/ProcessLine/detail/{lineId}
export const fetchProductProcessLineDetail = cache(async (lineId: number) => {
return serverFetchJson<ProductProcessLineDetailResponse>(
`${BASE_API_URL}/product-process/Demo/ProcessLine/detail/${lineId}`,
{
method: "GET",
}
);
});
export const updateProductProcessLineQrscan = cache(async (request: ProductProcessLineQrscanUpadteRequest) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/update`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
}
);
});
export const fetchAllJoborderProductProcessInfo = cache(async () => {
return serverFetchJson<AllJoborderProductProcessInfoResponse[]>(
`${BASE_API_URL}/product-process/Demo/Process/all`,
{
method: "GET",
next: { tags: ["productProcess"] },
}
);
});
export const updateProductProcessLineQty = async (request: UpdateProductProcessLineQtyRequest) => {
return serverFetchJson<UpdateProductProcessLineQtyResponse>(
`${BASE_API_URL}/product-process/lines/${request.productProcessLineId}/update/qty`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
}
);
};
export const startProductProcessLine = async (lineId: number, userId: number) => {
return serverFetchJson<ProductProcessLineResponse>(
`${BASE_API_URL}/product-process/lines/${lineId}/start?userId=${userId}`,


+ 24
- 22
src/app/api/pickOrder/actions.ts View File

@@ -95,12 +95,12 @@ export interface GetPickOrderInfoResponse {
export interface GetPickOrderInfo {
id: number;
code: string;
consoCode: string | null; // 添加 consoCode 属性
targetDate: string | number[]; // Support both formats
consoCode: string | null; // 添加 consoCode 属性
targetDate: string | number[]; // Support both formats
type: string;
status: string;
assignTo: number;
groupName: string; // Add this field
groupName: string; // Add this field
pickOrderLines: GetPickOrderLineInfo[];
}

@@ -256,16 +256,16 @@ export interface stockReponse{
noLot: boolean;
}
export interface FGPickOrderResponse {
// 新增:支持多个 pick orders
// 新增:支持多个 pick orders
doPickOrderId: number;
pickOrderIds?: number[];
pickOrderCodes?: string[]; // 改为数组
pickOrderCodes?: string[]; // 改为数组
deliveryOrderIds?: number[];
deliveryNos?: string[]; // 改为数组
deliveryNos?: string[]; // 改为数组
numberOfPickOrders?: number;
lineCountsPerPickOrder?: number[];// 新增:pick order 数量
lineCountsPerPickOrder?: number[];// 新增:pick order 数量
// 保留原有字段用于向后兼容(显示第一个 pick order)
// 保留原有字段用于向后兼容(显示第一个 pick order)
pickOrderId: number;
pickOrderCode: string;
pickOrderConsoCode: string;
@@ -332,17 +332,17 @@ export interface UpdateDoPickOrderHideStatusRequest {
}
export interface CompletedDoPickOrderResponse {
id: number;
doPickOrderRecordId: number; // 新增
doPickOrderRecordId: number; // 新增
pickOrderId: number;
pickOrderIds: number[]; // 新增:所有 pick order IDs
pickOrderIds: number[]; // 新增:所有 pick order IDs
pickOrderCode: string;
pickOrderCodes: string; // 新增:所有 pick order codes (逗号分隔)
pickOrderCodes: string; // 新增:所有 pick order codes (逗号分隔)
pickOrderConsoCode: string;
pickOrderStatus: string;
deliveryOrderId: number;
deliveryOrderIds: number[]; // 新增:所有 delivery order IDs
deliveryOrderIds: number[]; // 新增:所有 delivery order IDs
deliveryNo: string;
deliveryNos: string; // 新增:所有 delivery order codes (逗号分隔)
deliveryNos: string; // 新增:所有 delivery order codes (逗号分隔)
deliveryDate: string;
shopId: number;
shopCode: string;
@@ -352,13 +352,13 @@ export interface CompletedDoPickOrderResponse {
shopPoNo: string;
numberOfCartons: number;
truckLanceCode: string;
DepartureTime: string; // 新增
DepartureTime: string; // 新增
storeId: string;
completedDate: string;
fgPickOrders: FGPickOrderResponse[];
}

// 新增:搜索参数接口
// 新增:搜索参数接口
export interface CompletedDoPickOrderSearchParams {
pickOrderCode?: string;
shopName?: string;
@@ -491,7 +491,8 @@ export async function assignByLane(
userId: number,
storeId: string,
truckLanceCode: string,
truckDepartureTime?: string
truckDepartureTime?: string,
requiredDate?: string
): Promise<any> {
const response = await serverFetchJson(
`${BASE_API_URL}/doPickOrder/assign-by-lane`,
@@ -505,12 +506,13 @@ export async function assignByLane(
storeId,
truckLanceCode,
truckDepartureTime,
requiredDate,
}),
}
);
return response;
}
// 新增:获取已完成的 DO Pick Orders API
// 新增:获取已完成的 DO Pick Orders API
export const fetchCompletedDoPickOrders = async (
userId: number,
searchParams?: CompletedDoPickOrderSearchParams
@@ -919,7 +921,7 @@ export const fetchAllPickOrderLotsHierarchical = cache(async (userId: number): P
}
);
console.log(" Fetched hierarchical lot details:", data);
console.log(" Fetched hierarchical lot details:", data);
return data;
} catch (error) {
console.error("❌ Error fetching hierarchical lot details:", error);
@@ -947,7 +949,7 @@ export const fetchLotDetailsByDoPickOrderRecordId = async (doPickOrderRecordId:
}
);
console.log(" Fetched hierarchical lot details:", data);
console.log(" Fetched hierarchical lot details:", data);
return data;
} catch (error) {
console.error("❌ Error fetching lot details:", error);
@@ -962,7 +964,7 @@ export const fetchALLPickOrderLineLotDetails = cache(async (userId: number): Pro
try {
console.log("🔍 Fetching all pick order line lot details for userId:", userId);
// Use the non-auto-assign endpoint
// Use the non-auto-assign endpoint
const data = await serverFetchJson<any[]>(
`${BASE_API_URL}/pickOrder/all-lots-with-details-no-auto-assign/${userId}`,
{
@@ -971,7 +973,7 @@ export const fetchALLPickOrderLineLotDetails = cache(async (userId: number): Pro
}
);
console.log(" Fetched lot details:", data);
console.log(" Fetched lot details:", data);
return data;
} catch (error) {
console.error("❌ Error fetching lot details:", error);
@@ -987,7 +989,7 @@ export const fetchAllPickOrderDetails = cache(async (userId?: number) => {
};
}
// Use the correct endpoint with userId in the path
// Use the correct endpoint with userId in the path
const url = `${BASE_API_URL}/pickOrder/detail-optimized/${userId}`;
return serverFetchJson<GetPickOrderInfoResponse>(


+ 12
- 12
src/components/DoDetail/DoDetail.tsx View File

@@ -13,7 +13,7 @@ import { releaseDo, assignPickOrderByStore, releaseAssignedPickOrderByStore } fr
import DoInfoCard from "./DoInfoCard";
import DoLineTable from "./DoLineTable";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig"; // Import the correct session type
import { SessionWithTokens } from "@/config/authConfig"; // Import the correct session type

type Props = {
id?: number;
@@ -30,9 +30,9 @@ const DoDetail: React.FC<Props> = ({
const [serverError, setServerError] = useState("");
const [successMessage, setSuccessMessage] = useState("");
const [isAssigning, setIsAssigning] = useState(false);
const { data: session } = useSession() as { data: SessionWithTokens | null }; // Use correct session type
const { data: session } = useSession() as { data: SessionWithTokens | null }; // Use correct session type
const currentUserId = session?.id ? parseInt(session.id) : undefined; // Get user ID from session.id
const currentUserId = session?.id ? parseInt(session.id) : undefined; // Get user ID from session.id
console.log("🔍 DoSearch - session:", session);
console.log("🔍 DoSearch - currentUserId:", currentUserId);
const formProps = useForm<DoDetailType>({
@@ -50,7 +50,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);
setSuccessMessage("")
if (id) {
// Get current user ID from session
// Get current user ID from session
const currentUserId = session?.id ? parseInt(session.id) : undefined;
if (!currentUserId) {
@@ -60,7 +60,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);
const response = await releaseDo({
id: id,
userId: currentUserId // Pass user ID from session
userId: currentUserId // Pass user ID from session
})
if (response) {
@@ -74,16 +74,16 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);
} finally {
setIsUploading(false)
}
}, [id, formProps, t, setIsUploading, session]) // Add session to dependencies
}, [id, formProps, t, setIsUploading, session]) // Add session to dependencies

// UPDATE STORE-BASED ASSIGNMENT HANDLERS
// UPDATE STORE-BASED ASSIGNMENT HANDLERS
const handleAssignByStore = useCallback(async (storeId: string) => {
try {
setIsAssigning(true)
setServerError("")
setSuccessMessage("")
// Get current user ID from session
// Get current user ID from session
const currentUserId = session?.id ? parseInt(session.id) : undefined;
if (!currentUserId) {
@@ -107,7 +107,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);
} finally {
setIsAssigning(false)
}
}, [t, session]) // Add session to dependencies
}, [t, session]) // Add session to dependencies

const handleReleaseByStore = useCallback(async (storeId: string) => {
try {
@@ -115,7 +115,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);
setServerError("")
setSuccessMessage("")
// Get current user ID from session
// Get current user ID from session
const currentUserId = session?.id ? parseInt(session.id) : undefined;
if (!currentUserId) {
@@ -139,7 +139,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);
} finally {
setIsAssigning(false)
}
}, [t, session]) // Add session to dependencies
}, [t, session]) // Add session to dependencies

const onSubmit = useCallback<SubmitHandler<DoDetailType>>(async (data, event) => {
console.log(data)
@@ -182,7 +182,7 @@ console.log("🔍 DoSearch - currentUserId:", currentUserId);
</Stack>
)}
{/* ADD STORE-BASED ASSIGNMENT BUTTONS */}
{/* ADD STORE-BASED ASSIGNMENT BUTTONS */}
{
formProps.watch("status")?.toLowerCase() === "released" && (
<Box sx={{ mb: 2 }}>


+ 3
- 3
src/components/FinishedGoodSearch/CombinedLotTable.tsx View File

@@ -34,7 +34,7 @@ interface CombinedLotTableProps {
onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

// Simple helper function to check if item is completed
// Simple helper function to check if item is completed
const isItemCompleted = (lot: any) => {
const actualPickQty = Number(lot.actualPickQty) || 0;
const requiredQty = Number(lot.requiredQty) || 0;
@@ -60,7 +60,7 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({
}) => {
const { t } = useTranslation("pickOrder");

// Paginated data
// Paginated data
const paginatedLotData = useMemo(() => {
const startIndex = paginationController.pageNum * paginationController.pageSize;
const endIndex = startIndex + paginationController.pageSize;
@@ -113,7 +113,7 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({
const isCompleted = isItemCompleted(lot);
const isRejected = isItemRejected(lot);
// Green text color for completed items
// Green text color for completed items
const textColor = isCompleted ? 'success.main' : isRejected ? 'error.main' : 'inherit';
return (


+ 6
- 5
src/components/FinishedGoodSearch/FGPickOrderCard.tsx View File

@@ -47,7 +47,8 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
const handleAssignByLane = useCallback(async (
storeId: string,
truckDepartureTime: string,
truckLanceCode: string
truckLanceCode: string,
requiredDate: string
) => {
if (!currentUserId) {
console.error("Missing user id in session");
@@ -56,10 +57,10 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
setIsAssigning(true);
try {
const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime);
const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime, requiredDate);
if (res.code === "SUCCESS") {
console.log(" Successfully assigned pick order from lane", truckLanceCode);
console.log(" Successfully assigned pick order from lane", truckLanceCode);
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
loadSummaries(); // 刷新按钮状态
onPickOrderAssigned?.();
@@ -231,7 +232,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
variant="outlined"
size="medium"
disabled={item.lane.unassigned === 0 || isAssigning}
onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode)}
onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)}
sx={{
flex: 1,
fontSize: '1.1rem',
@@ -344,7 +345,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
variant="outlined"
size="medium"
disabled={item.lane.unassigned === 0 || isAssigning}
onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode)}
onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)}
sx={{
flex: 1,
fontSize: '1.1rem',


+ 6
- 6
src/components/FinishedGoodSearch/FGPickOrderInfoCard.tsx View File

@@ -27,22 +27,22 @@ const FGPickOrderInfoCard: React.FC<Props> = ({ fgOrder, doPickOrderDetail }) =>
<Grid item xs={6}>
<TextField
value={pickOrderCodes || ""} // 显示所有 pick order codes
label={t("Pick Order Code(s)")} // 修改标签
value={pickOrderCodes || ""} // 显示所有 pick order codes
label={t("Pick Order Code(s)")} // 修改标签
fullWidth
disabled={true}
multiline={pickOrderCodes.includes(',')} // 如果有多个代码,使用多行
multiline={pickOrderCodes.includes(',')} // 如果有多个代码,使用多行
rows={pickOrderCodes.includes(',') ? 2 : 1}
/>
</Grid>
<Grid item xs={6}>
<TextField
value={deliveryOrderCodes || ""} // 显示所有 delivery order codes
label={t("Delivery Order Code(s)")} // 修改标签
value={deliveryOrderCodes || ""} // 显示所有 delivery order codes
label={t("Delivery Order Code(s)")} // 修改标签
fullWidth
disabled={true}
multiline={deliveryOrderCodes.includes(',')} // 如果有多个代码,使用多行
multiline={deliveryOrderCodes.includes(',')} // 如果有多个代码,使用多行
rows={deliveryOrderCodes.includes(',') ? 2 : 1}
/>
</Grid>


+ 25
- 16
src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx View File

@@ -56,22 +56,31 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
loadSummaries();
}, [loadSummaries]);

const handleAssignByLane = useCallback(async (
storeId: string,
truckDepartureTime: string,
truckLanceCode: string
) => {
if (!currentUserId) {
console.error("Missing user id in session");
return;
}
setIsAssigning(true);
try {
const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime);
const handleAssignByLane = useCallback(async (
storeId: string,
truckDepartureTime: string,
truckLanceCode: string,
requiredDate: string

) => {
if (!currentUserId) {
console.error("Missing user id in session");
return;
}
let dateParam: string | undefined;
if (requiredDate === "today") {
dateParam = dayjs().format('YYYY-MM-DD');
} else if (requiredDate === "tomorrow") {
dateParam = dayjs().add(1, 'day').format('YYYY-MM-DD');
} else if (requiredDate === "dayAfterTomorrow") {
dateParam = dayjs().add(2, 'day').format('YYYY-MM-DD');
}
setIsAssigning(true);
try {
const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime, dateParam);
if (res.code === "SUCCESS") {
console.log("✅ Successfully assigned pick order from lane", truckLanceCode);
console.log(" Successfully assigned pick order from lane", truckLanceCode);
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
loadSummaries(); // 刷新按钮状态
onPickOrderAssigned?.();
@@ -236,7 +245,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
variant="outlined"
size="medium"
disabled={item.lane.unassigned === 0 || isAssigning}
onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode)}
onClick={() => handleAssignByLane("2/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)}
sx={{
flex: 1,
fontSize: '1.1rem',
@@ -336,7 +345,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned }) =>
variant="outlined"
size="medium"
disabled={item.lane.unassigned === 0 || isAssigning}
onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode)}
onClick={() => handleAssignByLane("4/F", item.truckDepartureTime, item.lane.truckLanceCode, selectedDate)}
sx={{
flex: 1,
fontSize: '1.1rem',


+ 6
- 6
src/components/FinishedGoodSearch/FinishedGoodSearch.tsx View File

@@ -247,7 +247,7 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
const handleCompletionStatusChange = (event: CustomEvent) => {
const { allLotsCompleted, tabIndex: eventTabIndex } = event.detail;
// 修复:根据标签页和事件来源决定是否更新打印按钮状态
// 修复:根据标签页和事件来源决定是否更新打印按钮状态
if (eventTabIndex === undefined || eventTabIndex === tabIndex) {
setPrintButtonsEnabled(allLotsCompleted);
console.log(`Print buttons enabled for tab ${tabIndex}:`, allLotsCompleted);
@@ -259,9 +259,9 @@ const PickOrderSearch: React.FC<Props> = ({ pickOrders }) => {
return () => {
window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
};
}, [tabIndex]); // 添加 tabIndex 依赖
}, [tabIndex]); // 添加 tabIndex 依赖

// 新增:处理标签页切换时的打印按钮状态重置
// 新增:处理标签页切换时的打印按钮状态重置
useEffect(() => {
// 当切换到标签页 2 (GoodPickExecutionRecord) 时,重置打印按钮状态
if (tabIndex === 2) {
@@ -286,7 +286,7 @@ const handleAssignByLane = useCallback(async (
const res = await assignByLane(currentUserId, storeId, truckLanceCode, truckDepartureTime);
if (res.code === "SUCCESS") {
console.log(" Successfully assigned pick order from lane", truckLanceCode);
console.log(" Successfully assigned pick order from lane", truckLanceCode);
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
loadSummaries(); // 刷新按钮状态
} else if (res.code === "USER_BUSY") {
@@ -322,7 +322,7 @@ const handleAssignByLane = useCallback(async (
setIsAssigning(false);
}
}, [currentUserId, t, loadSummaries]);
// Manual assignment handler - uses the action function
// Manual assignment handler - uses the action function
*/
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
@@ -607,7 +607,7 @@ const handleAssignByLane = useCallback(async (
</Grid>
</Box>

{/* Tabs section - Move the click handler here */}
{/* Tabs section - Move the click handler here */}
<Box sx={{
borderBottom: '1px solid #e0e0e0'
}}>


+ 53
- 53
src/components/FinishedGoodSearch/GoodPickExecution.tsx View File

@@ -26,7 +26,7 @@ import {
updateStockOutLineStatus,
createStockOutLine,
recordPickExecutionIssue,
fetchFGPickOrdersByUserId, // Add this import
fetchFGPickOrdersByUserId, // Add this import
FGPickOrderResponse,
autoAssignAndReleasePickOrder,
AutoAssignReleaseResponse,
@@ -59,13 +59,13 @@ interface Props {
onFgPickOrdersChange?: (fgPickOrders: FGPickOrderResponse[]) => void;
}

// QR Code Modal Component (from LotTable)
// QR Code Modal Component (from LotTable)
const QrCodeModal: React.FC<{
open: boolean;
onClose: () => void;
lot: any | null;
onQrCodeSubmit: (lotNo: string) => void;
combinedLotData: any[]; // Add this prop
combinedLotData: any[]; // Add this prop
}> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
const { t } = useTranslation("pickOrder");
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
@@ -105,7 +105,7 @@ const QrCodeModal: React.FC<{
setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
if (stockInLineInfo.lotNo === lot.lotNo) {
console.log(` QR Code verified for lot: ${lot.lotNo}`);
console.log(` QR Code verified for lot: ${lot.lotNo}`);
setQrScanSuccess(true);
onQrCodeSubmit(lot.lotNo);
onClose();
@@ -297,7 +297,7 @@ const QrCodeModal: React.FC<{

{qrScanSuccess && (
<Typography variant="caption" color="success" display="block">
{t("Verified successfully!")}
{t("Verified successfully!")}
</Typography>
)}
</Box>
@@ -348,11 +348,11 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
const formProps = useForm();
const errors = formProps.formState.errors;

// Add QR modal states
// Add QR modal states
const [qrModalOpen, setQrModalOpen] = useState(false);
const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);

// Add GoodPickExecutionForm states
// Add GoodPickExecutionForm states
const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
@@ -389,14 +389,14 @@ const fetchFgPickOrdersData = useCallback(async () => {
}
}, [currentUserId, selectedPickOrderId]);
// 简化:移除复杂的 useEffect 依赖
// 简化:移除复杂的 useEffect 依赖
useEffect(() => {
if (currentUserId) {
fetchFgPickOrdersData();
}
}, [currentUserId, fetchFgPickOrdersData]);

// Handle QR code button click
// Handle QR code button click
const handleQrCodeClick = (pickOrderId: number) => {
console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
// TODO: Implement QR code functionality
@@ -424,22 +424,22 @@ const fetchFgPickOrdersData = useCallback(async () => {
return;
}
// Use the non-auto-assign endpoint - this only fetches existing data
// Use the non-auto-assign endpoint - this only fetches existing data
const allLotDetails = await fetchALLPickOrderLineLotDetails(userIdToUse);
console.log(" All combined lot details:", allLotDetails);
console.log(" All combined lot details:", allLotDetails);
setCombinedLotData(allLotDetails);
setOriginalCombinedData(allLotDetails);
// 计算完成状态并发送事件
// 计算完成状态并发送事件
const allCompleted = allLotDetails.length > 0 && allLotDetails.every(lot =>
lot.processingStatus === 'completed'
);
// 发送完成状态事件,包含标签页信息
// 发送完成状态事件,包含标签页信息
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: allCompleted,
tabIndex: 0 // 明确指定这是来自标签页 0 的事件
tabIndex: 0 // 明确指定这是来自标签页 0 的事件
}
}));
@@ -448,7 +448,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
setCombinedLotData([]);
setOriginalCombinedData([]);
// 如果加载失败,禁用打印按钮
// 如果加载失败,禁用打印按钮
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
@@ -460,18 +460,18 @@ const fetchFgPickOrdersData = useCallback(async () => {
}
}, [currentUserId, combinedLotData]);

// Only fetch existing data when session is ready, no auto-assignment
// Only fetch existing data when session is ready, no auto-assignment
useEffect(() => {
if (session && currentUserId && !initializationRef.current) {
console.log(" Session loaded, initializing pick order...");
console.log(" Session loaded, initializing pick order...");
initializationRef.current = true;
// Only fetch existing data, no auto-assignment
// Only fetch existing data, no auto-assignment
fetchAllCombinedLotData();
}
}, [session, currentUserId, fetchAllCombinedLotData]);

// Add event listener for manual assignment
// Add event listener for manual assignment
useEffect(() => {
const handlePickOrderAssigned = () => {
console.log("🔄 Pick order assigned event received, refreshing data...");
@@ -485,12 +485,12 @@ const fetchFgPickOrdersData = useCallback(async () => {
};
}, [fetchAllCombinedLotData]);

// Handle QR code submission for matched lot (external scanning)
// Handle QR code submission for matched lot (external scanning)
// Handle QR code submission for matched lot (external scanning)
// Handle QR code submission for matched lot (external scanning)
const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
console.log(` Processing QR Code for lot: ${lotNo}`);
console.log(` Processing QR Code for lot: ${lotNo}`);
// Use current data without refreshing to avoid infinite loop
// Use current data without refreshing to avoid infinite loop
const currentLotData = combinedLotData;
console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
@@ -506,7 +506,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
return;
}
console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
setQrScanError(false);
try {
@@ -518,7 +518,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
console.log(`🔄 Processing pick order line ${matchingLot.pickOrderLineId} for lot ${lotNo}`);
if (matchingLot.stockOutLineId) {
console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
existsCount++;
} else {
const stockOutLineData: CreateStockOutLine = {
@@ -533,10 +533,10 @@ const fetchFgPickOrdersData = useCallback(async () => {
console.log(`Create stock out line result for line ${matchingLot.pickOrderLineId}:`, result);
if (result && result.code === "EXISTS") {
console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
console.log(` Stock out line already exists for line ${matchingLot.pickOrderLineId}`);
existsCount++;
} else if (result && result.code === "SUCCESS") {
console.log(` Stock out line created successfully for line ${matchingLot.pickOrderLineId}`);
console.log(` Stock out line created successfully for line ${matchingLot.pickOrderLineId}`);
successCount++;
} else {
console.error(`❌ Failed to create stock out line for line ${matchingLot.pickOrderLineId}:`, result);
@@ -545,16 +545,16 @@ const fetchFgPickOrdersData = useCallback(async () => {
}
}
// Always refresh data after processing (success or failure)
// Always refresh data after processing (success or failure)
console.log("🔄 Refreshing data after QR code processing...");
await fetchAllCombinedLotData();
if (successCount > 0 || existsCount > 0) {
console.log(` QR Code processing completed: ${successCount} created, ${existsCount} already existed`);
console.log(` QR Code processing completed: ${successCount} created, ${existsCount} already existed`);
setQrScanSuccess(true);
setQrScanInput(''); // Clear input after successful processing
// Clear success state after a delay
// Clear success state after a delay
setTimeout(() => {
setQrScanSuccess(false);
}, 2000);
@@ -563,7 +563,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
setQrScanError(true);
setQrScanSuccess(false);
// Clear error state after a delay
// Clear error state after a delay
setTimeout(() => {
setQrScanError(false);
}, 3000);
@@ -573,10 +573,10 @@ const fetchFgPickOrdersData = useCallback(async () => {
setQrScanError(true);
setQrScanSuccess(false);
// Still refresh data even on error
// Still refresh data even on error
await fetchAllCombinedLotData();
// Clear error state after a delay
// Clear error state after a delay
setTimeout(() => {
setQrScanError(false);
}, 3000);
@@ -589,17 +589,17 @@ const fetchFgPickOrdersData = useCallback(async () => {
}
}, [qrScanInput, handleQrCodeSubmit]);

// Handle QR code submission from modal (internal scanning)
// Handle QR code submission from modal (internal scanning)
const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
console.log(` QR Code verified for lot: ${lotNo}`);
console.log(` QR Code verified for lot: ${lotNo}`);
const requiredQty = selectedLotForQr.requiredQty;
const lotId = selectedLotForQr.lotId;
// Create stock out line
const stockOutLineData: CreateStockOutLine = {
consoCode: selectedLotForQr.pickOrderConsoCode, // Use pickOrderConsoCode instead of pickOrderCode
consoCode: selectedLotForQr.pickOrderConsoCode, // Use pickOrderConsoCode instead of pickOrderCode
pickOrderLineId: selectedLotForQr.pickOrderLineId,
inventoryLotLineId: selectedLotForQr.lotId,
qty: 0.0
@@ -620,7 +620,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
...prev,
[lotKey]: requiredQty
}));
console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
}, 500);
// Refresh data
@@ -631,7 +631,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
}
}, [selectedLotForQr, fetchAllCombinedLotData]);

// Outside QR scanning - process QR codes from outside the page automatically
// Outside QR scanning - process QR codes from outside the page automatically
useEffect(() => {
if (qrValues.length > 0 && combinedLotData.length > 0) {
const latestQr = qrValues[qrValues.length - 1];
@@ -707,7 +707,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
console.log("Found completed pick orders, auto-assigning next...");
// 移除前端的自动分配逻辑,因为后端已经处理了
// 移除前端的自动分配逻辑,因为后端已经处理了
// await handleAutoAssignAndRelease(); // 删除这个函数
}
} catch (error) {
@@ -715,7 +715,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
}
}, [currentUserId]);

// Handle submit pick quantity
// Handle submit pick quantity
const handleSubmitPickQty = useCallback(async (lot: any) => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const newQty = pickQtyData[lotKey] || 0;
@@ -759,14 +759,14 @@ const fetchFgPickOrdersData = useCallback(async () => {
});
}
// FIXED: Use the proper API function instead of direct fetch
// FIXED: Use the proper API function instead of direct fetch
if (newStatus === 'completed' && lot.pickOrderConsoCode) {
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
try {
// Use the imported API function instead of direct fetch
// Use the imported API function instead of direct fetch
const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
console.log(` Pick order completion check result:`, completionResponse);
console.log(` Pick order completion check result:`, completionResponse);
if (completionResponse.code === "SUCCESS") {
console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`);
@@ -792,7 +792,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
}
}, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]);

// Handle reject lot
// Handle reject lot
const handleRejectLot = useCallback(async (lot: any) => {
if (!lot.stockOutLineId) {
console.error("No stock out line found for this lot");
@@ -818,7 +818,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
}
}, [fetchAllCombinedLotData, checkAndAutoAssignNext]);

// Handle pick execution form
// Handle pick execution form
const handlePickExecutionForm = useCallback((lot: any) => {
console.log("=== Pick Execution Form ===");
console.log("Lot data:", lot);
@@ -847,7 +847,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
console.log("Pick execution issue recorded:", result);
if (result && result.code === "SUCCESS") {
console.log(" Pick execution issue recorded successfully");
console.log(" Pick execution issue recorded successfully");
} else {
console.error("❌ Failed to record pick execution issue:", result);
}
@@ -861,7 +861,7 @@ const fetchFgPickOrdersData = useCallback(async () => {
}
}, [fetchAllCombinedLotData]);

// Calculate remaining required quantity
// Calculate remaining required quantity
const calculateRemainingRequiredQty = useCallback((lot: any) => {
const requiredQty = lot.requiredQty || 0;
const stockOutLineQty = lot.stockOutLineQty || 0;
@@ -942,7 +942,7 @@ const fetchFgPickOrdersData = useCallback(async () => {

// Pagination data with sorting by routerIndex
const paginatedData = useMemo(() => {
// Sort by routerIndex first, then by other criteria
// Sort by routerIndex first, then by other criteria
const sortedData = [...combinedLotData].sort((a, b) => {
const aIndex = a.routerIndex || 0;
const bIndex = b.routerIndex || 0;
@@ -970,14 +970,14 @@ const fetchFgPickOrdersData = useCallback(async () => {

return (
<FormProvider {...formProps}>
{/* 修复:改进条件渲染逻辑 */}
{/* 修复:改进条件渲染逻辑 */}
{combinedDataLoading || fgPickOrdersLoading ? (
// 数据加载中,显示加载指示器
// 数据加载中,显示加载指示器
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress />
</Box>
) : fgPickOrders.length === 0 ? (
// 没有活动订单,显示楼层选择面板
// 没有活动订单,显示楼层选择面板
<FinishedGoodFloorLanePanel
onPickOrderAssigned={() => {
if (currentUserId) {
@@ -987,7 +987,7 @@ return (
}}
/>
) : (
// 有活动订单,显示 FG 订单信息
// 有活动订单,显示 FG 订单信息
<Box>
{fgPickOrders.map((fgOrder) => (
<Box key={fgOrder.pickOrderId} sx={{ mb: 2 }}>


+ 13
- 13
src/components/FinishedGoodSearch/GoodPickExecutionForm.tsx View File

@@ -53,7 +53,7 @@ interface PickExecutionFormProps {
selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
pickOrderId?: number;
pickOrderCreateDate: any;
// Remove these props since we're not handling normal cases
// Remove these props since we're not handling normal cases
// onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
// selectedRowId?: number | null;
}
@@ -75,7 +75,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
selectedPickOrderLine,
pickOrderId,
pickOrderCreateDate,
// Remove these props
// Remove these props
// onNormalPickSubmit,
// selectedRowId,
}) => {
@@ -86,11 +86,11 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
const [handlers, setHandlers] = useState<Array<{ id: number; name: string }>>([]);
// 计算剩余可用数量
const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => {
// 直接使用 availableQty,因为 API 没有返回 inQty 和 outQty
// 直接使用 availableQty,因为 API 没有返回 inQty 和 outQty
return lot.availableQty || 0;
}, []);
const calculateRequiredQty = useCallback((lot: LotPickData) => {
// Use the original required quantity, not subtracting actualPickQty
// Use the original required quantity, not subtracting actualPickQty
// The actualPickQty in the form should be independent of the database value
return lot.requiredQty || 0;
}, []);
@@ -175,7 +175,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
}
}, [errors]);

// Update form validation to require either missQty > 0 OR badItemQty > 0
// Update form validation to require either missQty > 0 OR badItemQty > 0
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
const req = selectedLot?.requiredQty || 0;
@@ -200,10 +200,10 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
};

const handleSubmit = async () => {
// ✅ 先验证表单
// First validate the form
if (!validateForm()) {
console.error('Form validation failed:', errors);
return; // ✅ 阻止提交,显示验证错误
return; // Prevent submission, show validation errors
}
if (!formData.pickOrderId) {
@@ -214,10 +214,10 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
setLoading(true);
try {
await onSubmit(formData as PickExecutionIssueData);
// ✅ 成功时会自动关闭(由 onClose 处理)
// Automatically closed when successful (handled by onClose)
} catch (error: any) {
console.error('Error submitting pick execution issue:', error);
// ✅ 显示错误消息(可以通过 props 或 state 传递错误消息到父组件)
// Show error message (can be passed to parent component via props or state)
// 或者在这里显示 toast/alert
alert(t('Failed to submit issue. Please try again.') + (error.message ? `: ${error.message}` : ''));
} finally {
@@ -241,11 +241,11 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
{t('Pick Execution Issue Form')} {/* Always show issue form title */}
{t('Pick Execution Issue Form')} {/* Always show issue form title */}
</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2 }}>
{/* Add instruction text */}
{/* Add instruction text */}
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
@@ -255,7 +255,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
</Box>
</Grid>
{/* Keep the existing form fields */}
{/* Keep the existing form fields */}
<Grid item xs={6}>
<TextField
fullWidth
@@ -317,7 +317,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
/>
</Grid>
{/* Show issue description and handler fields when bad items > 0 */}
{/* Show issue description and handler fields when bad items > 0 */}
{(formData.badItemQty && formData.badItemQty > 0) ? (
<>
<Grid item xs={12}>


+ 30
- 30
src/components/FinishedGoodSearch/GoodPickExecutionRecord.tsx View File

@@ -72,7 +72,7 @@ interface Props {
}


// 新增:Pick Order 数据接口
// 新增:Pick Order 数据接口
interface PickOrderData {
pickOrderId: number;
pickOrderCode: string;
@@ -89,20 +89,20 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
const currentUserId = session?.id ? parseInt(session.id) : undefined;
// 新增:已完成 DO Pick Orders 状态
// 新增:已完成 DO Pick Orders 状态
const [completedDoPickOrders, setCompletedDoPickOrders] = useState<CompletedDoPickOrderResponse[]>([]);
const [completedDoPickOrdersLoading, setCompletedDoPickOrdersLoading] = useState(false);
// 新增:详情视图状态
// 新增:详情视图状态
const [selectedDoPickOrder, setSelectedDoPickOrder] = useState<CompletedDoPickOrderResponse | null>(null);
const [showDetailView, setShowDetailView] = useState(false);
const [detailLotData, setDetailLotData] = useState<any[]>([]);
// 新增:搜索状态
// 新增:搜索状态
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
const [filteredDoPickOrders, setFilteredDoPickOrders] = useState<CompletedDoPickOrderResponse[]>([]);
// 新增:分页状态
// 新增:分页状态
const [paginationController, setPaginationController] = useState({
pageNum: 0,
pageSize: 10,
@@ -307,7 +307,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
}
}, [t]);

// 修改:使用新的 API 获取已完成的 DO Pick Orders
// 修改:使用新的 API 获取已完成的 DO Pick Orders
const fetchCompletedDoPickOrdersData = useCallback(async (searchParams?: CompletedDoPickOrderSearchParams) => {
if (!currentUserId) return;
@@ -319,7 +319,7 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
setCompletedDoPickOrders(completedDoPickOrders);
setFilteredDoPickOrders(completedDoPickOrders);
console.log(" Fetched completed DO pick orders:", completedDoPickOrders);
console.log(" Fetched completed DO pick orders:", completedDoPickOrders);
} catch (error) {
console.error("❌ Error fetching completed DO pick orders:", error);
setCompletedDoPickOrders([]);
@@ -329,14 +329,14 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
}
}, [currentUserId]);

// 初始化时获取数据
// 初始化时获取数据
useEffect(() => {
if (currentUserId) {
fetchCompletedDoPickOrdersData();
}
}, [currentUserId, fetchCompletedDoPickOrdersData]);

// 修改:搜索功能使用新的 API
// 修改:搜索功能使用新的 API
const handleSearch = useCallback((query: Record<string, any>) => {
setSearchQuery({ ...query });
console.log("Search query:", query);
@@ -352,13 +352,13 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
fetchCompletedDoPickOrdersData(searchParams);
}, [fetchCompletedDoPickOrdersData]);

// 修复:重命名函数避免重复声明
// 修复:重命名函数避免重复声明
const handleSearchReset = useCallback(() => {
setSearchQuery({});
fetchCompletedDoPickOrdersData(); // 重新获取所有数据
}, [fetchCompletedDoPickOrdersData]);

// 分页功能
// 分页功能
const handlePageChange = useCallback((event: unknown, newPage: number) => {
setPaginationController(prev => ({
...prev,
@@ -374,14 +374,14 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
});
}, []);

// 分页数据
// 分页数据
const paginatedData = useMemo(() => {
const startIndex = paginationController.pageNum * paginationController.pageSize;
const endIndex = startIndex + paginationController.pageSize;
return filteredDoPickOrders.slice(startIndex, endIndex);
}, [filteredDoPickOrders, paginationController]);

// 搜索条件
// 搜索条件
const searchCriteria: Criterion<any>[] = [
{
label: t("Pick Order Code"),
@@ -405,11 +405,11 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => {
setShowDetailView(true);
try {
// 使用 doPickOrderRecordId 而不是 pickOrderId
// 使用 doPickOrderRecordId 而不是 pickOrderId
const hierarchicalData = await fetchLotDetailsByDoPickOrderRecordId(doPickOrder.doPickOrderRecordId);
console.log(" Loaded hierarchical lot data:", hierarchicalData);
console.log(" Loaded hierarchical lot data:", hierarchicalData);
// 转换为平铺格式
// 转换为平铺格式
const flatLotData: any[] = [];

if (hierarchicalData.pickOrders && hierarchicalData.pickOrders.length > 0) {
@@ -465,7 +465,7 @@ if (hierarchicalData.pickOrders && hierarchicalData.pickOrders.length > 0) {
}
});
} else if (lineStockouts.length > 0) {
// lots 为空但有 stockouts(如「雞絲碗仔翅」),也要显示
// lots 为空但有 stockouts(如「雞絲碗仔翅」),也要显示
lineStockouts.forEach((so: any) => {
flatLotData.push({
pickOrderCode: po.pickOrderCode,
@@ -488,7 +488,7 @@ if (hierarchicalData.pickOrders && hierarchicalData.pickOrders.length > 0) {

setDetailLotData(flatLotData);
// 计算完成状态
// 计算完成状态
const allCompleted = flatLotData.length > 0 && flatLotData.every(lot =>
lot.processingStatus === 'completed'
);
@@ -500,7 +500,7 @@ setDetailLotData(flatLotData);
}
}));
} catch (error) { // 添加 catch 块
} catch (error) { // 添加 catch 块
console.error("❌ Error loading detail lot data:", error);
setDetailLotData([]);
@@ -514,13 +514,13 @@ setDetailLotData(flatLotData);
}, []);


// 返回列表视图
// 返回列表视图
const handleBackToList = useCallback(() => {
setShowDetailView(false);
setSelectedDoPickOrder(null);
setDetailLotData([]);
// 返回列表时禁用打印按钮
// 返回列表时禁用打印按钮
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
@@ -530,8 +530,8 @@ setDetailLotData(flatLotData);
}, []);


// 如果显示详情视图,渲染类似 GoodPickExecution 的表格
// 如果显示详情视图,渲染层级结构
// 如果显示详情视图,渲染类似 GoodPickExecution 的表格
// 如果显示详情视图,渲染层级结构
if (showDetailView && selectedDoPickOrder) {
return (
<FormProvider {...formProps}>
@@ -567,7 +567,7 @@ if (showDetailView && selectedDoPickOrder) {
</Stack>
</Paper>

{/* 添加:多个 Pick Orders 信息(如果有) */}
{/* 添加:多个 Pick Orders 信息(如果有) */}
{selectedDoPickOrder.pickOrderIds && selectedDoPickOrder.pickOrderIds.length > 1 && (
<Paper sx={{ mb: 2, p: 2, backgroundColor: '#f5f5f5' }}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
@@ -586,7 +586,7 @@ if (showDetailView && selectedDoPickOrder) {
</Paper>
)}

{/* 数据检查 */}
{/* 数据检查 */}
{detailLotData.length === 0 ? (
<Box sx={{ p: 3, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary">
@@ -594,16 +594,16 @@ if (showDetailView && selectedDoPickOrder) {
</Typography>
</Box>
) : (
/* 按 Pick Order 分组显示 */
/* 按 Pick Order 分组显示 */
<Stack spacing={2}>
{/* 按 pickOrderCode 分组 */}
{/* 按 pickOrderCode 分组 */}
{Object.entries(
detailLotData.reduce((acc: any, lot: any) => {
const key = lot.pickOrderCode || 'Unknown';
if (!acc[key]) {
acc[key] = {
lots: [],
deliveryOrderCode: lot.deliveryOrderCode || 'N/A' // 保存对应的 deliveryOrderCode
deliveryOrderCode: lot.deliveryOrderCode || 'N/A' // 保存对应的 deliveryOrderCode
};
}
acc[key].lots.push(lot);
@@ -615,7 +615,7 @@ if (showDetailView && selectedDoPickOrder) {
<Typography variant="subtitle1" fontWeight="bold">
{t("Pick Order")}: {pickOrderCode} ({data.lots.length} {t("items")})
{" | "}
{t("Delivery Order")}: {data.deliveryOrderCode} {/* 使用保存的 deliveryOrderCode */}
{t("Delivery Order")}: {data.deliveryOrderCode} {/* 使用保存的 deliveryOrderCode */}
</Typography>
</AccordionSummary>
<AccordionDetails>
@@ -665,7 +665,7 @@ if (showDetailView && selectedDoPickOrder) {
);
}

// 默认列表视图
// 默认列表视图
return (
<FormProvider {...formProps}>
<Box>


+ 131
- 131
src/components/FinishedGoodSearch/GoodPickExecutiondetail.tsx View File

@@ -32,7 +32,7 @@ import {
createStockOutLine,
updateStockOutLine,
recordPickExecutionIssue,
fetchFGPickOrders, // Add this import
fetchFGPickOrders, // Add this import
FGPickOrderResponse,
stockReponse,
PickExecutionIssueData,
@@ -42,8 +42,8 @@ import {
checkAndCompletePickOrderByConsoCode,
updateSuggestedLotLineId,
confirmLotSubstitution,
fetchDoPickOrderDetail, // 必须添加
DoPickOrderDetail, // 必须添加
fetchDoPickOrderDetail, // 必须添加
DoPickOrderDetail, // 必须添加
fetchFGPickOrdersByUserId
} from "@/app/api/pickOrder/actions";

@@ -70,13 +70,13 @@ interface Props {
filterArgs: Record<string, any>;
}

// QR Code Modal Component (from LotTable)
// QR Code Modal Component (from LotTable)
const QrCodeModal: React.FC<{
open: boolean;
onClose: () => void;
lot: any | null;
onQrCodeSubmit: (lotNo: string) => void;
combinedLotData: any[]; // Add this prop
combinedLotData: any[]; // Add this prop
}> = ({ open, onClose, lot, onQrCodeSubmit, combinedLotData }) => {
const { t } = useTranslation("pickOrder");
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
@@ -116,7 +116,7 @@ const QrCodeModal: React.FC<{
setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
if (stockInLineInfo.lotNo === lot.lotNo) {
console.log(` QR Code verified for lot: ${lot.lotNo}`);
console.log(` QR Code verified for lot: ${lot.lotNo}`);
setQrScanSuccess(true);
onQrCodeSubmit(lot.lotNo);
onClose();
@@ -308,7 +308,7 @@ const QrCodeModal: React.FC<{

{qrScanSuccess && (
<Typography variant="caption" color="success" display="block">
{t("Verified successfully!")}
{t("Verified successfully!")}
</Typography>
)}
</Box>
@@ -359,20 +359,20 @@ const [pickOrderSwitching, setPickOrderSwitching] = useState(false);
const formProps = useForm();
const errors = formProps.formState.errors;

// Add QR modal states
// Add QR modal states
const [qrModalOpen, setQrModalOpen] = useState(false);
const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);
const [lotConfirmationOpen, setLotConfirmationOpen] = useState(false);
const [expectedLotData, setExpectedLotData] = useState<any>(null);
const [scannedLotData, setScannedLotData] = useState<any>(null);
const [isConfirmingLot, setIsConfirmingLot] = useState(false);
// Add GoodPickExecutionForm states
// Add GoodPickExecutionForm states
const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);

const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);
// Add these missing state variables after line 352
// Add these missing state variables after line 352
const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
@@ -381,7 +381,7 @@ const [isConfirmingLot, setIsConfirmingLot] = useState(false);
// Handle QR code button click
// Handle QR code button click
const handleQrCodeClick = (pickOrderId: number) => {
console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
// TODO: Implement QR code functionality
@@ -435,11 +435,11 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
return;
}
// 获取新结构的层级数据
// 获取新结构的层级数据
const hierarchicalData = await fetchAllPickOrderLotsHierarchical(userIdToUse);
console.log(" Hierarchical data (new structure):", hierarchicalData);
console.log(" Hierarchical data (new structure):", hierarchicalData);
// 检查数据结构
// 检查数据结构
if (!hierarchicalData.fgInfo || !hierarchicalData.pickOrders || hierarchicalData.pickOrders.length === 0) {
console.warn("⚠️ No FG info or pick orders found");
setCombinedLotData([]);
@@ -448,10 +448,10 @@ const fetchAllCombinedLotData = useCallback(async (userId?: number, pickOrderIdO
return;
}
// 使用合并后的 pick order 对象(现在只有一个对象,包含所有数据)
// 使用合并后的 pick order 对象(现在只有一个对象,包含所有数据)
const mergedPickOrder = hierarchicalData.pickOrders[0];
// 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片)
// 设置 FG info 到 fgPickOrders(用于显示 FG 信息卡片)
// 修改第 478-509 行的 fgOrder 构建逻辑:

const fgOrder: FGPickOrderResponse = {
@@ -464,7 +464,7 @@ const fgOrder: FGPickOrderResponse = {
DepartureTime: hierarchicalData.fgInfo.departureTime,
shopAddress: "",
pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
// 兼容字段
// 兼容字段
pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0,
pickOrderConsoCode: mergedPickOrder.consoCode || "",
pickOrderTargetDate: mergedPickOrder.targetDate || "",
@@ -477,16 +477,16 @@ const fgOrder: FGPickOrderResponse = {
numberOfCartons: mergedPickOrder.pickOrderLines?.length || 0,
qrCodeData: hierarchicalData.fgInfo.doPickOrderId,
// 新增:多个 pick orders 信息 - 保持数组格式,不要 join
// 新增:多个 pick orders 信息 - 保持数组格式,不要 join
numberOfPickOrders: mergedPickOrder.pickOrderIds?.length || 0,
pickOrderIds: mergedPickOrder.pickOrderIds || [],
pickOrderCodes: Array.isArray(mergedPickOrder.pickOrderCodes)
? mergedPickOrder.pickOrderCodes
: [], // 改:保持数组
: [], // 改:保持数组
deliveryOrderIds: mergedPickOrder.doOrderIds || [],
deliveryNos: Array.isArray(mergedPickOrder.deliveryOrderCodes)
? mergedPickOrder.deliveryOrderCodes
: [], // 改:保持数组
: [], // 改:保持数组
lineCountsPerPickOrder: Array.isArray(mergedPickOrder.lineCountsPerPickOrder)
? mergedPickOrder.lineCountsPerPickOrder
: []
@@ -499,38 +499,38 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
// ❌ 移除:不需要 doPickOrderDetail 和 switcher 逻辑
// if (hierarchicalData.pickOrders.length > 1) { ... }
// 直接使用合并后的 pickOrderLines
// 直接使用合并后的 pickOrderLines
console.log("🎯 Displaying merged pick order lines");
// 将层级数据转换为平铺格式(用于表格显示)
// 将层级数据转换为平铺格式(用于表格显示)
const flatLotData: any[] = [];
mergedPickOrder.pickOrderLines.forEach((line: any) => {
if (line.lots && line.lots.length > 0) {
// 修复:先对 lots 按 lotId 去重并合并 requiredQty
// 修复:先对 lots 按 lotId 去重并合并 requiredQty
const lotMap = new Map<number, any>();
line.lots.forEach((lot: any) => {
const lotId = lot.id;
if (lotMap.has(lotId)) {
// 如果已存在,合并 requiredQty
// 如果已存在,合并 requiredQty
const existingLot = lotMap.get(lotId);
existingLot.requiredQty = (existingLot.requiredQty || 0) + (lot.requiredQty || 0);
// 保留其他字段(使用第一个遇到的 lot 的字段)
// 保留其他字段(使用第一个遇到的 lot 的字段)
} else {
// 首次遇到,添加到 map
// 首次遇到,添加到 map
lotMap.set(lotId, { ...lot });
}
});
// 遍历去重后的 lots
// 遍历去重后的 lots
lotMap.forEach((lot: any) => {
flatLotData.push({
// 使用合并后的数据
// 使用合并后的数据
pickOrderConsoCode: mergedPickOrder.consoCode,
pickOrderTargetDate: mergedPickOrder.targetDate,
pickOrderStatus: mergedPickOrder.status,
pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0, // 使用第一个 pickOrderId
pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0, // 使用第一个 pickOrderId
pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
pickOrderLineId: line.id,
pickOrderLineRequiredQty: line.requiredQty,
@@ -548,7 +548,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
location: lot.location,
stockUnit: lot.stockUnit,
availableQty: lot.availableQty,
requiredQty: lot.requiredQty, // 使用合并后的 requiredQty
requiredQty: lot.requiredQty, // 使用合并后的 requiredQty
actualPickQty: lot.actualPickQty,
inQty: lot.inQty,
outQty: lot.outQty,
@@ -569,16 +569,16 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
});
});
} else {
// 没有 lots 的情况(null stock)- 从 stockouts 数组中获取 id
// 没有 lots 的情况(null stock)- 从 stockouts 数组中获取 id
const firstStockout = line.stockouts && line.stockouts.length > 0
? line.stockouts[0]
: null;
flatLotData.push({
pickOrderConsoCode: mergedPickOrder.consoCodes?.[0] || "", // 修复:consoCodes 是数组
pickOrderConsoCode: mergedPickOrder.consoCodes?.[0] || "", // 修复:consoCodes 是数组
pickOrderTargetDate: mergedPickOrder.targetDate,
pickOrderStatus: mergedPickOrder.status,
pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0, // 使用第一个 pickOrderId
pickOrderId: mergedPickOrder.pickOrderIds?.[0] || 0, // 使用第一个 pickOrderId
pickOrderCode: mergedPickOrder.pickOrderCodes?.[0] || "",
pickOrderLineId: line.id,
pickOrderLineRequiredQty: line.requiredQty,
@@ -590,7 +590,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
uomDesc: line.item.uomDesc,
uomShortDesc: line.item.uomShortDesc,
// Null stock 字段 - 从 stockouts 数组中获取
// Null stock 字段 - 从 stockouts 数组中获取
lotId: firstStockout?.lotId || null,
lotNo: firstStockout?.lotNo || null,
expiryDate: null,
@@ -606,7 +606,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
lotAvailability: 'insufficient_stock',
processingStatus: firstStockout?.status || 'pending',
suggestedPickLotId: null,
stockOutLineId: firstStockout?.id || null, // 使用 stockouts 数组中的 id
stockOutLineId: firstStockout?.id || null, // 使用 stockouts 数组中的 id
stockOutLineStatus: firstStockout?.status || null,
stockOutLineQty: firstStockout?.qty || 0,
@@ -619,7 +619,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
}
});

console.log(" Transformed flat lot data:", flatLotData);
console.log(" Transformed flat lot data:", flatLotData);
console.log("🔍 Total items (including null stock):", flatLotData.length);
setCombinedLotData(flatLotData);
@@ -635,25 +635,25 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
setCombinedDataLoading(false);
}
}, [currentUserId, checkAllLotsCompleted]); // ❌ 移除 selectedPickOrderId 依赖
// Add effect to check completion when lot data changes
// Add effect to check completion when lot data changes
useEffect(() => {
if (combinedLotData.length > 0) {
checkAllLotsCompleted(combinedLotData);
}
}, [combinedLotData, checkAllLotsCompleted]);

// Add function to expose completion status to parent
// Add function to expose completion status to parent
const getCompletionStatus = useCallback(() => {
return allLotsCompleted;
}, [allLotsCompleted]);

// Expose completion status to parent component
// Expose completion status to parent component
useEffect(() => {
// Dispatch custom event with completion status
const event = new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted,
tabIndex: 1 // 明确指定这是来自标签页 1 的事件
tabIndex: 1 // 明确指定这是来自标签页 1 的事件
}
});
window.dispatchEvent(event);
@@ -708,22 +708,22 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
}
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchAllCombinedLotData]);
const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
console.log(` Processing QR Code for lot: ${lotNo}`);
console.log(` Processing QR Code for lot: ${lotNo}`);
// 检查 lotNo 是否为 null 或 undefined(包括字符串 "null")
// 检查 lotNo 是否为 null 或 undefined(包括字符串 "null")
if (!lotNo || lotNo === 'null' || lotNo.trim() === '') {
console.error("❌ Invalid lotNo: null, undefined, or empty");
return;
}
// Use current data without refreshing to avoid infinite loop
// Use current data without refreshing to avoid infinite loop
const currentLotData = combinedLotData;
console.log(` Available lots:`, currentLotData.map(lot => lot.lotNo));
// 修复:在比较前确保 lotNo 不为 null
// 修复:在比较前确保 lotNo 不为 null
const lotNoLower = lotNo.toLowerCase();
const matchingLots = currentLotData.filter(lot => {
if (!lot.lotNo) return false; // 跳过 null lotNo
if (!lot.lotNo) return false; // 跳过 null lotNo
return lot.lotNo === lotNo || lot.lotNo.toLowerCase() === lotNoLower;
});
@@ -736,7 +736,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
return;
}
console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
setQrScanError(false);
try {
@@ -811,20 +811,20 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
}
}
// FIXED: Set refresh flag before refreshing data
// FIXED: Set refresh flag before refreshing data
setIsRefreshingData(true);
console.log("🔄 Refreshing data after QR code processing...");
await fetchAllCombinedLotData();
if (successCount > 0) {
console.log(` QR Code processing completed: ${successCount} updated/created`);
console.log(` QR Code processing completed: ${successCount} updated/created`);
setQrScanSuccess(true);
setQrScanError(false);
setQrScanInput(''); // Clear input after successful processing
//setIsManualScanning(false);
// stopScan();
// resetScan();
// Clear success state after a delay
// Clear success state after a delay
//setTimeout(() => {
//setQrScanSuccess(false);
@@ -834,7 +834,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
setQrScanError(true);
setQrScanSuccess(false);
// Clear error state after a delay
// Clear error state after a delay
// setTimeout(() => {
// setQrScanError(false);
//}, 3000);
@@ -844,16 +844,16 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
setQrScanError(true);
setQrScanSuccess(false);
// Still refresh data even on error
// Still refresh data even on error
setIsRefreshingData(true);
await fetchAllCombinedLotData();
// Clear error state after a delay
// Clear error state after a delay
setTimeout(() => {
setQrScanError(false);
}, 3000);
} finally {
// Clear refresh flag after a short delay
// Clear refresh flag after a short delay
setTimeout(() => {
setIsRefreshingData(false);
}, 1000);
@@ -914,7 +914,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
return;
}
// FIXED: Find the ACTIVE suggested lot (not rejected lots)
// FIXED: Find the ACTIVE suggested lot (not rejected lots)
const activeSuggestedLots = sameItemLotsInExpected.filter(lot =>
lot.lotAvailability !== 'rejected' &&
lot.stockOutLineStatus !== 'rejected' &&
@@ -942,7 +942,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
}
// Case 2: Item matches but lot number differs -> open confirmation modal
// FIXED: Use the first ACTIVE suggested lot, not just any lot
// FIXED: Use the first ACTIVE suggested lot, not just any lot
const expectedLot = activeSuggestedLots[0];
if (!expectedLot) {
console.error("Could not determine expected lot for confirmation");
@@ -951,7 +951,7 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
return;
}
// Check if the expected lot is already the scanned lot (after substitution)
// Check if the expected lot is already the scanned lot (after substitution)
if (expectedLot.lotNo === scanned?.lotNo) {
console.log(`Lot already substituted, proceeding with ${scanned.lotNo}`);
handleQrCodeSubmit(scanned.lotNo);
@@ -981,8 +981,8 @@ console.log("🔍 DEBUG fgOrder.deliveryNos:", fgOrder.deliveryNos);
return;
}
}, [combinedLotData, handleQrCodeSubmit, handleLotMismatch]);
// Update the outside QR scanning effect to use enhanced processing
// Update the outside QR scanning effect to use enhanced processing
// Update the outside QR scanning effect to use enhanced processing
// Update the outside QR scanning effect to use enhanced processing
useEffect(() => {
if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
return;
@@ -1003,18 +1003,18 @@ useEffect(() => {
processOutsideQrCode(latestQr);
}
}, [qrValues, isManualScanning, processedQrCodes, lastProcessedQr, isRefreshingData, processOutsideQrCode, combinedLotData]);
// Only fetch existing data when session is ready, no auto-assignment
// Only fetch existing data when session is ready, no auto-assignment
useEffect(() => {
if (session && currentUserId && !initializationRef.current) {
console.log(" Session loaded, initializing pick order...");
console.log(" Session loaded, initializing pick order...");
initializationRef.current = true;
// Only fetch existing data, no auto-assignment
// Only fetch existing data, no auto-assignment
fetchAllCombinedLotData();
}
}, [session, currentUserId, fetchAllCombinedLotData]);

// Add event listener for manual assignment
// Add event listener for manual assignment
useEffect(() => {
const handlePickOrderAssigned = () => {
console.log("🔄 Pick order assigned event received, refreshing data...");
@@ -1036,10 +1036,10 @@ useEffect(() => {
}
}, [qrScanInput, handleQrCodeSubmit]);

// Handle QR code submission from modal (internal scanning)
// Handle QR code submission from modal (internal scanning)
const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
console.log(` QR Code verified for lot: ${lotNo}`);
console.log(` QR Code verified for lot: ${lotNo}`);
const requiredQty = selectedLotForQr.requiredQty;
const lotId = selectedLotForQr.lotId;
@@ -1068,7 +1068,7 @@ useEffect(() => {
...prev,
[lotKey]: requiredQty
}));
console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
}, 500);
// Refresh data
@@ -1117,7 +1117,7 @@ useEffect(() => {
if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
console.log("Found completed pick orders, auto-assigning next...");
// 移除前端的自动分配逻辑,因为后端已经处理了
// 移除前端的自动分配逻辑,因为后端已经处理了
// await handleAutoAssignAndRelease(); // 删除这个函数
}
} catch (error) {
@@ -1125,7 +1125,7 @@ useEffect(() => {
}
}, [currentUserId]);

// Handle submit pick quantity
// Handle submit pick quantity
const handleSubmitPickQty = useCallback(async (lot: any) => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const newQty = pickQtyData[lotKey] || 0;
@@ -1136,11 +1136,11 @@ useEffect(() => {
}
try {
// FIXED: Calculate cumulative quantity correctly
// FIXED: Calculate cumulative quantity correctly
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + newQty;
// FIXED: Determine status based on cumulative quantity vs required quantity
// FIXED: Determine status based on cumulative quantity vs required quantity
let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
@@ -1163,7 +1163,7 @@ useEffect(() => {
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: newStatus,
qty: cumulativeQty // Use cumulative quantity
qty: cumulativeQty // Use cumulative quantity
});
if (newQty > 0) {
@@ -1175,13 +1175,13 @@ useEffect(() => {
});
}
// Check if pick order is completed when lot status becomes 'completed'
// Check if pick order is completed when lot status becomes 'completed'
if (newStatus === 'completed' && lot.pickOrderConsoCode) {
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
try {
const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
console.log(` Pick order completion check result:`, completionResponse);
console.log(` Pick order completion check result:`, completionResponse);
if (completionResponse.code === "SUCCESS") {
console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`);
@@ -1207,7 +1207,7 @@ useEffect(() => {
}
}, [pickQtyData, fetchAllCombinedLotData, checkAndAutoAssignNext]);

// Handle reject lot
// Handle reject lot
const handleRejectLot = useCallback(async (lot: any) => {
if (!lot.stockOutLineId) {
console.error("No stock out line found for this lot");
@@ -1233,7 +1233,7 @@ useEffect(() => {
}
}, [fetchAllCombinedLotData, checkAndAutoAssignNext]);

// Handle pick execution form
// Handle pick execution form
const handlePickExecutionForm = useCallback((lot: any) => {
console.log("=== Pick Execution Form ===");
console.log("Lot data:", lot);
@@ -1264,7 +1264,7 @@ useEffect(() => {
console.log("Pick execution issue recorded:", result);
if (result && result.code === "SUCCESS") {
console.log(" Pick execution issue recorded successfully");
console.log(" Pick execution issue recorded successfully");
} else {
console.error("❌ Failed to record pick execution issue:", result);
}
@@ -1285,7 +1285,7 @@ useEffect(() => {
}
}, [fetchAllCombinedLotData]);

// Calculate remaining required quantity
// Calculate remaining required quantity
const calculateRemainingRequiredQty = useCallback((lot: any) => {
const requiredQty = lot.requiredQty || 0;
const stockOutLineQty = lot.stockOutLineQty || 0;
@@ -1369,7 +1369,7 @@ useEffect(() => {
const paginatedData = useMemo(() => {
const startIndex = paginationController.pageNum * paginationController.pageSize;
const endIndex = startIndex + paginationController.pageSize;
return combinedLotData.slice(startIndex, endIndex); // No sorting needed
return combinedLotData.slice(startIndex, endIndex); // No sorting needed
}, [combinedLotData, paginationController]);
const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
if (!lot.stockOutLineId) {
@@ -1378,11 +1378,11 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
}
try {
// FIXED: Calculate cumulative quantity correctly
// FIXED: Calculate cumulative quantity correctly
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
// FIXED: Determine status based on cumulative quantity vs required quantity
// FIXED: Determine status based on cumulative quantity vs required quantity
let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
@@ -1405,7 +1405,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: newStatus,
qty: cumulativeQty // Use cumulative quantity
qty: cumulativeQty // Use cumulative quantity
});
if (submitQty > 0) {
@@ -1417,13 +1417,13 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
});
}
// Check if pick order is completed when lot status becomes 'completed'
// Check if pick order is completed when lot status becomes 'completed'
if (newStatus === 'completed' && lot.pickOrderConsoCode) {
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
try {
const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
console.log(` Pick order completion check result:`, completionResponse);
console.log(` Pick order completion check result:`, completionResponse);
if (completionResponse.code === "SUCCESS") {
console.log(`�� Pick order ${lot.pickOrderConsoCode} completed successfully!`);
@@ -1450,7 +1450,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
}, [fetchAllCombinedLotData, checkAndAutoAssignNext]);


// Add these functions after line 395
// Add these functions after line 395
const handleStartScan = useCallback(() => {
console.log(" Starting manual QR scan...");
setIsManualScanning(true);
@@ -1468,7 +1468,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
console.log("🔍 Switching to pick order:", pickOrderId);
setSelectedPickOrderId(pickOrderId);
// 强制刷新数据,确保显示正确的 pick order 数据
// 强制刷新数据,确保显示正确的 pick order 数据
await fetchAllCombinedLotData(currentUserId, pickOrderId);
} catch (error) {
console.error("Error switching pick order:", error);
@@ -1487,7 +1487,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
}, [stopScan, resetScan]);
// ... existing code around line 1469 ...
const handlelotnull = useCallback(async (lot: any) => {
// 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId
// 优先使用 stockouts 中的 id,如果没有则使用 stockOutLineId
const stockOutLineId = lot.stockOutLineId;
if (!stockOutLineId) {
@@ -1496,14 +1496,14 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
}
try {
// Step 1: Update stock out line status
// Step 1: Update stock out line status
await updateStockOutLineStatus({
id: stockOutLineId,
status: 'completed',
qty: 0
});
// Step 2: Create pick execution issue for no-lot case
// Step 2: Create pick execution issue for no-lot case
// Get pick order ID from fgPickOrders or use 0 if not available
const pickOrderId = lot.pickOrderId || fgPickOrders[0]?.pickOrderId || 0;
const pickOrderCode = lot.pickOrderCode || fgPickOrders[0]?.pickOrderCode || lot.pickOrderConsoCode || '';
@@ -1512,18 +1512,18 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
type: "Do", // Delivery Order type
pickOrderId: pickOrderId,
pickOrderCode: pickOrderCode,
pickOrderCreateDate: dayjs().format('YYYY-MM-DD'), // Use dayjs format
pickOrderCreateDate: dayjs().format('YYYY-MM-DD'), // Use dayjs format
pickExecutionDate: dayjs().format('YYYY-MM-DD'),
pickOrderLineId: lot.pickOrderLineId,
itemId: lot.itemId,
itemCode: lot.itemCode || '',
itemDescription: lot.itemName || '',
lotId: null, // No lot available
lotNo: null, // No lot number
lotId: null, // No lot available
lotNo: null, // No lot number
storeLocation: lot.location || '',
requiredQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0,
actualPickQty: 0, // No items picked (no lot available)
missQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, // All quantity is missing
actualPickQty: 0, // No items picked (no lot available)
missQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0, // All quantity is missing
badItemQty: 0,
issueRemark: `No lot available for this item. Handled via handlelotnull.`,
pickerName: session?.user?.name || '',
@@ -1531,15 +1531,15 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
};
const result = await recordPickExecutionIssue(issueData);
console.log(" Pick execution issue created for no-lot item:", result);
console.log(" Pick execution issue created for no-lot item:", result);
if (result && result.code === "SUCCESS") {
console.log(" No-lot item handled and issue recorded successfully");
console.log(" No-lot item handled and issue recorded successfully");
} else {
console.error("❌ Failed to record pick execution issue:", result);
}
// Step 3: Refresh data
// Step 3: Refresh data
await fetchAllCombinedLotData();
} catch (error) {
console.error("❌ Error in handlelotnull:", error);
@@ -1548,14 +1548,14 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
// ... existing code ...
const handleSubmitAllScanned = useCallback(async () => {
const scannedLots = combinedLotData.filter(lot => {
// 如果是 noLot 情况,检查状态是否为 pending 或 partially_complete
// 如果是 noLot 情况,检查状态是否为 pending 或 partially_complete
if (lot.noLot === true) {
return lot.stockOutLineStatus === 'checked' ||
lot.stockOutLineStatus === 'pending' ||
lot.stockOutLineStatus === 'partially_completed' ||
lot.stockOutLineStatus === 'PARTIALLY_COMPLETE';
}
// 正常情况:只包含 checked 状态
// 正常情况:只包含 checked 状态
return lot.stockOutLineStatus === 'checked';
});
@@ -1568,35 +1568,35 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`);
try {
// Submit all items in parallel using Promise.all
// Submit all items in parallel using Promise.all
const submitPromises = scannedLots.map(async (lot) => {
// 检查是否是 noLot 情况
// 检查是否是 noLot 情况
if (lot.noLot === true) {
// 使用 handlelotnull 处理无 lot 的情况
// 使用 handlelotnull 处理无 lot 的情况
console.log(`Submitting no-lot item: ${lot.itemName || lot.itemCode}`);
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: 'completed',
qty: 0
});
console.log(` No-lot item completed: ${lot.itemName || lot.itemCode}`);
console.log(` No-lot item completed: ${lot.itemName || lot.itemCode}`);
const pickOrderId = lot.pickOrderId || fgPickOrders[0]?.pickOrderId || 0;
const pickOrderCode = lot.pickOrderCode || fgPickOrders[0]?.pickOrderCode || lot.pickOrderConsoCode || '';
const issueData: PickExecutionIssueData = {
type: "Do", // Delivery Order type
pickOrderId: pickOrderId,
pickOrderCode: pickOrderCode,
pickOrderCreateDate: dayjs().format('YYYY-MM-DD'), // Use dayjs format
pickOrderCreateDate: dayjs().format('YYYY-MM-DD'), // Use dayjs format
pickExecutionDate: dayjs().format('YYYY-MM-DD'),
pickOrderLineId: lot.pickOrderLineId,
itemId: lot.itemId,
itemCode: lot.itemCode || '',
itemDescription: lot.itemName || '',
lotId: null, // No lot available
lotNo: null, // No lot number
lotId: null, // No lot available
lotNo: null, // No lot number
storeLocation: lot.location || '',
requiredQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0,
actualPickQty: 0, // No items picked (no lot available)
actualPickQty: 0, // No items picked (no lot available)
missQty: lot.requiredQty || lot.pickOrderLineRequiredQty || 0,
badItemQty: 0,
issueRemark: `No lot available for this item. Handled via handlelotnull.`,
@@ -1607,7 +1607,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
return { success: true, lotNo: lot.lotNo || 'No Lot', isNoLot: true };
}
// 正常情况:有 lot 的处理逻辑
// 正常情况:有 lot 的处理逻辑
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
@@ -1644,13 +1644,13 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
return { success: true, lotNo: lot.lotNo };
});
// Wait for all submissions to complete
// Wait for all submissions to complete
const results = await Promise.all(submitPromises);
const successCount = results.filter(r => r.success).length;
console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`);
console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`);
// Refresh data once after all submissions
// Refresh data once after all submissions
await fetchAllCombinedLotData();
if (successCount > 0) {
@@ -1669,11 +1669,11 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
}
}, [combinedLotData, fetchAllCombinedLotData, checkAndAutoAssignNext, handlelotnull]);

// Calculate scanned items count
// Calculate scanned items count (should match handleSubmitAllScanned filter logic)
// Calculate scanned items count
// Calculate scanned items count (should match handleSubmitAllScanned filter logic)
const scannedItemsCount = useMemo(() => {
const filtered = combinedLotData.filter(lot => {
// 如果是 noLot 情况,只要状态不是 completed 或 rejected,就包含
// 如果是 noLot 情况,只要状态不是 completed 或 rejected,就包含
if (lot.noLot === true) {
const status = lot.stockOutLineStatus?.toLowerCase();
const include = status !== 'completed' && status !== 'rejected';
@@ -1682,11 +1682,11 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
}
return include;
}
// 正常情况:只包含 checked 状态
// 正常情况:只包含 checked 状态
return lot.stockOutLineStatus === 'checked';
});
// 添加调试日志
// 添加调试日志
const noLotCount = filtered.filter(l => l.noLot === true).length;
const normalCount = filtered.filter(l => l.noLot !== true).length;
console.log(`📊 scannedItemsCount calculation: total=${filtered.length}, noLot=${noLotCount}, normal=${normalCount}`);
@@ -1699,7 +1699,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
return filtered.length;
}, [combinedLotData]);

// ADD THIS: Auto-stop scan when no data available
// ADD THIS: Auto-stop scan when no data available
useEffect(() => {
if (isManualScanning && combinedLotData.length === 0) {
console.log("⏹️ No data available, auto-stopping QR scan...");
@@ -1707,7 +1707,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
}
}, [combinedLotData.length, isManualScanning, handleStopScan]);

// Cleanup effect
// Cleanup effect
useEffect(() => {
return () => {
// Cleanup when component unmounts (e.g., when switching tabs)
@@ -1754,7 +1754,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
{/* 保留:Combined Lot Table - 包含所有 QR 扫描功能 */}
{/* 保留:Combined Lot Table - 包含所有 QR 扫描功能 */}
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="h6" gutterBottom sx={{ mb: 0 }}>
@@ -1784,7 +1784,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
</Button>
)}
{/* 保留:Submit All Scanned Button */}
{/* 保留:Submit All Scanned Button */}
<Button
variant="contained"
color="success"
@@ -1824,9 +1824,9 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
</Typography>
</Stack>
{/* 改进:三个字段显示在一起,使用表格式布局 */}
{/* 改进:三个字段合并显示 */}
{/* 改进:表格式显示每个 pick order */}
{/* 改进:三个字段显示在一起,使用表格式布局 */}
{/* 改进:三个字段合并显示 */}
{/* 改进:表格式显示每个 pick order */}
<Box sx={{
p: 2,
backgroundColor: '#f5f5f5',
@@ -1861,7 +1861,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
return <Typography variant="body2" color="text.secondary">-</Typography>;
}
// 使用与外部基本信息相同的样式
// 使用与外部基本信息相同的样式
return Array.from({ length: maxLength }, (_, idx) => (
<Stack
key={idx}
@@ -1915,7 +1915,7 @@ const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: numbe
) : (
// 在第 1797-1938 行之间,将整个 map 函数修改为:
paginatedData.map((lot, index) => {
// 检查是否是 issue lot
// 检查是否是 issue lot
const isIssueLot = lot.stockOutLineStatus === 'rejected' || !lot.lotNo;
return (
@@ -1961,7 +1961,7 @@ paginatedData.map((lot, index) => {
</TableCell>
<TableCell align="center">
{/* Issue lot 不显示扫描状态 */}
{/* Issue lot 不显示扫描状态 */}
{!isIssueLot && lot.stockOutLineStatus?.toLowerCase() !== 'pending' ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Checkbox
@@ -1996,7 +1996,7 @@ paginatedData.map((lot, index) => {
<TableCell align="center">
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
{isIssueLot ? (
// Issue lot 只显示 Issue 按钮
// Issue lot 只显示 Issue 按钮
<Button
variant="outlined"
size="small"
@@ -2017,7 +2017,7 @@ paginatedData.map((lot, index) => {
{t("Issue")}
</Button>
) : (
// Normal lot 显示两个按钮
// Normal lot 显示两个按钮
<Stack direction="row" spacing={1} alignItems="center">
<Button
variant="contained"
@@ -2090,7 +2090,7 @@ paginatedData.map((lot, index) => {
</Box>
</Stack>
{/* 保留:QR Code Modal */}
{/* 保留:QR Code Modal */}
<QrCodeModal
open={qrModalOpen}
onClose={() => {
@@ -2104,7 +2104,7 @@ paginatedData.map((lot, index) => {
onQrCodeSubmit={handleQrCodeSubmitFromModal}
/>
{/* 保留:Lot Confirmation Modal */}
{/* 保留:Lot Confirmation Modal */}
{lotConfirmationOpen && expectedLotData && scannedLotData && (
<LotConfirmationModal
open={lotConfirmationOpen}
@@ -2120,7 +2120,7 @@ paginatedData.map((lot, index) => {
/>
)}
{/* 保留:Good Pick Execution Form Modal */}
{/* 保留:Good Pick Execution Form Modal */}
{pickExecutionFormOpen && selectedLotForExecutionForm && (
<GoodPickExecutionForm
open={pickExecutionFormOpen}


+ 38
- 38
src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx View File

@@ -33,7 +33,7 @@ import EscalationComponent from "../PoDetail/EscalationComponent";
import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions";
import {
updateInventoryLotLineStatus
} from "@/app/api/inventory/actions"; // 导入新的 API
} from "@/app/api/inventory/actions"; // 导入新的 API
import { dayjsToDateTimeString } from "@/app/utils/formatUtil";
import dayjs from "dayjs";

@@ -42,8 +42,8 @@ interface ExtendedQcItem extends QcItemWithChecks {
qcPassed?: boolean;
failQty?: number;
remarks?: string;
order?: number; // Add order property
stableId?: string; // Also add stableId for better row identification
order?: number; // Add order property
stableId?: string; // Also add stableId for better row identification
}
interface Props extends CommonProps {
itemDetail: GetPickOrderLineInfo & {
@@ -55,7 +55,7 @@ interface Props extends CommonProps {
selectedLotId?: number;
onStockOutLineUpdate?: () => void;
lotData: LotPickData[];
// Add missing props
// Add missing props
pickQtyData?: PickQtyData;
selectedRowId?: number;
}
@@ -104,7 +104,7 @@ interface Props extends CommonProps {
};
qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem
setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>; // Change to ExtendedQcItem
// Add props for stock out line update
// Add props for stock out line update
selectedLotId?: number;
onStockOutLineUpdate?: () => void;
lotData: LotPickData[];
@@ -193,7 +193,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
failQty: item.isPassed ? 0 : (item.failQty || 0), // 0 for passed, actual qty for failed
type: "pick_order_qc",
remarks: item.remarks || "",
qcPassed: item.isPassed, // This will now be included
qcPassed: item.isPassed, // This will now be included
}));

// Store the submitted data for debug display
@@ -217,17 +217,17 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
}
};

// 修改:在组件开始时自动设置失败数量
// 修改:在组件开始时自动设置失败数量
useEffect(() => {
if (itemDetail && qcItems.length > 0 && selectedLotId) {
// 获取选中的批次数据
// 获取选中的批次数据
const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId);
if (selectedLot) {
// 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty
// 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty
const updatedQcItems = qcItems.map((item, index) => ({
...item,
failQty: selectedLot.requiredQty || 0, // 使用 Lot Required Pick Qty
// Add stable order and ID fields
// Add stable order and ID fields
order: index,
stableId: `qc-${item.id}-${index}`
}));
@@ -236,7 +236,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
}
}, [itemDetail, qcItems.length, selectedLotId, lotData]);

// Add this helper function at the top of the component
// Add this helper function at the top of the component
const safeClose = useCallback(() => {
if (onClose) {
// Create a mock event object that satisfies the Modal onClose signature
@@ -259,12 +259,12 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
isPersistent: () => false
} as any;
// Fixed: Pass both event and reason parameters
// Fixed: Pass both event and reason parameters
onClose(mockEvent, 'escapeKeyDown'); // 'escapeKeyDown' is a valid reason
}
}, [onClose]);

// 修改:移除 alert 弹窗,改为控制台日志
// 修改:移除 alert 弹窗,改为控制台日志
const onSubmitQc = useCallback<SubmitHandler<any>>(
async (data, event) => {
setIsSubmitting(true);
@@ -276,7 +276,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
const validationErrors : string[] = [];
const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId);
// Add safety check for selectedLot
// Add safety check for selectedLot
if (!selectedLot) {
console.error("Selected lot not found");
return;
@@ -313,23 +313,23 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
return;
}
// Handle different QC decisions
// Handle different QC decisions
if (selectedLotId) {
try {
const allPassed = qcData.qcItems.every(item => item.isPassed);
if (qcDecision === "2") {
// QC Decision 2: Report and Re-pick
// QC Decision 2: Report and Re-pick
console.log("QC Decision 2 - Report and Re-pick: Rejecting lot and marking as unavailable");
// Inventory lot line status: unavailable
// Inventory lot line status: unavailable
if (selectedLot) {
try {
console.log("=== DEBUG: Updating inventory lot line status ===");
console.log("Selected lot:", selectedLot);
console.log("Selected lot ID:", selectedLotId);
// FIX: Only send the fields that the backend expects
// FIX: Only send the fields that the backend expects
const updateData = {
inventoryLotLineId: selectedLot.lotId,
status: 'unavailable'
@@ -339,7 +339,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
console.log("Update data:", updateData);
const result = await updateInventoryLotLineStatus(updateData);
console.log(" Inventory lot line status updated successfully:", result);
console.log(" Inventory lot line status updated successfully:", result);
} catch (error) {
console.error("❌ Error updating inventory lot line status:", error);
@@ -359,28 +359,28 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
return;
}
// Close modal and refresh data
safeClose(); // Fixed: Use safe close function with both parameters
// Close modal and refresh data
safeClose(); // Fixed: Use safe close function with both parameters
if (onStockOutLineUpdate) {
onStockOutLineUpdate();
}
} else if (qcDecision === "1") {
// QC Decision 1: Accept
// QC Decision 1: Accept
console.log("QC Decision 1 - Accept: QC passed");
// Stock out line status: checked (QC completed)
// Stock out line status: checked (QC completed)
await updateStockOutLineStatus({
id: selectedLotId,
status: 'checked',
qty: acceptQty || 0
});
// Inventory lot line status: NO CHANGE needed
// Inventory lot line status: NO CHANGE needed
// Keep the existing status from handleSubmitPickQty
// Close modal and refresh data
safeClose(); // Fixed: Use safe close function with both parameters
// Close modal and refresh data
safeClose(); // Fixed: Use safe close function with both parameters
if (onStockOutLineUpdate) {
onStockOutLineUpdate();
}
@@ -399,7 +399,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
console.log("QC results saved successfully!");
// Show warning dialog for failed QC items when accepting
// Show warning dialog for failed QC items when accepting
if (qcDecision === "1" && !qcData.qcItems.every((q) => q.isPassed)) {
submitDialogWithWarning(() => {
closeHandler?.({}, 'escapeKeyDown');
@@ -448,7 +448,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")}
onChange={(e) => {
const value = e.target.value === "true";
// Simple state update
// Simple state update
setQcItems(prev =>
prev.map(item =>
item.id === params.id
@@ -490,10 +490,10 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
<TextField
type="number"
size="small"
// 修改:失败项目自动显示 Lot Required Pick Qty
// 修改:失败项目自动显示 Lot Required Pick Qty
value={!params.row.qcPassed ? (0) : 0}
disabled={params.row.qcPassed}
// 移除 onChange,因为数量是固定的
// 移除 onChange,因为数量是固定的
// onChange={(e) => {
// const v = e.target.value;
// const next = v === "" ? undefined : Number(v);
@@ -535,7 +535,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
[t],
);

// Add stable update function
// Add stable update function
const handleQcResultChange = useCallback((itemId: number, qcPassed: boolean) => {
setQcItems(prevItems =>
prevItems.map(item =>
@@ -546,16 +546,16 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
);
}, []);

// Remove duplicate functions
// Remove duplicate functions
const getRowId = useCallback((row: any) => {
return row.id; // Just use the original ID
}, []);

// Remove complex sorting logic
// Remove complex sorting logic
// const stableQcItems = useMemo(() => { ... }); // Remove
// const sortedQcItems = useMemo(() => { ... }); // Remove

// Use qcItems directly in DataGrid
// Use qcItems directly in DataGrid
return (
<>
<FormProvider {...formProps}>
@@ -593,9 +593,9 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
<StyledDataGrid
columns={qcColumns}
rows={qcItems} // Use qcItems directly
rows={qcItems} // Use qcItems directly
autoHeight
getRowId={getRowId} // Simple row ID function
getRowId={getRowId} // Simple row ID function
/>
</Grid>
</>
@@ -636,7 +636,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
/>

{/* Combirne options 2 & 3 into one */}
{/* Combirne options 2 & 3 into one */}
<FormControlLabel
value="2"
control={<Radio />}
@@ -649,7 +649,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
</FormControl>
</Grid>

{/* Show escalation component when QC Decision = 2 (Report and Re-pick) */}
{/* Show escalation component when QC Decision = 2 (Report and Re-pick) */}

<Grid item xs={12} sx={{ mt: 2 }}>


+ 2
- 2
src/components/FinishedGoodSearch/newcreatitem.tsx View File

@@ -568,7 +568,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
return;
}
// 修复:自动填充 type 为 "Consumable",不再强制用户选择
// 修复:自动填充 type 为 "Consumable",不再强制用户选择
// if (!data.type) {
// alert(t("Please select product type"));
// return;
@@ -625,7 +625,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
}
}
// 修复:自动使用 "Consumable" 作为默认 type
// 修复:自动使用 "Consumable" 作为默认 type
const pickOrderData: SavePickOrderRequest = {
type: data.type || "Consumable", // 如果用户选择了 type 就用用户的,否则默认 "Consumable"
targetDate: formattedTargetDate,


+ 3
- 3
src/components/Jodetail/CombinedLotTable.tsx View File

@@ -34,7 +34,7 @@ interface CombinedLotTableProps {
onPageSizeChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}

// Simple helper function to check if item is completed
// Simple helper function to check if item is completed
const isItemCompleted = (lot: any) => {
const actualPickQty = Number(lot.actualPickQty) || 0;
const requiredQty = Number(lot.requiredQty) || 0;
@@ -60,7 +60,7 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({
}) => {
const { t } = useTranslation("pickOrder");

// Paginated data
// Paginated data
const paginatedLotData = useMemo(() => {
const startIndex = paginationController.pageNum * paginationController.pageSize;
const endIndex = startIndex + paginationController.pageSize;
@@ -113,7 +113,7 @@ const CombinedLotTable: React.FC<CombinedLotTableProps> = ({
const isCompleted = isItemCompleted(lot);
const isRejected = isItemRejected(lot);
// Green text color for completed items
// Green text color for completed items
const textColor = isCompleted ? 'success.main' : isRejected ? 'error.main' : 'inherit';
return (


+ 30
- 30
src/components/Jodetail/FInishedJobOrderRecord.tsx View File

@@ -24,7 +24,7 @@ import {
Accordion,
AccordionSummary,
AccordionDetails,
Checkbox, // Add Checkbox import
Checkbox, // Add Checkbox import
} from "@mui/material";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { useCallback, useEffect, useState, useRef, useMemo } from "react";
@@ -47,7 +47,7 @@ interface Props {
filterArgs: Record<string, any>;
}

// 修改:已完成的 Job Order Pick Order 接口
// 修改:已完成的 Job Order Pick Order 接口
interface CompletedJobOrderPickOrder {
id: number;
pickOrderId: number;
@@ -68,7 +68,7 @@ interface CompletedJobOrderPickOrder {
completedItems: number;
}

// 新增:Lot 详情接口
// 新增:Lot 详情接口
interface LotDetail {
lotId: number;
lotNo: string;
@@ -104,21 +104,21 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
const currentUserId = session?.id ? parseInt(session.id) : undefined;
// 修改:已完成 Job Order Pick Orders 状态
// 修改:已完成 Job Order Pick Orders 状态
const [completedJobOrderPickOrders, setCompletedJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]);
const [completedJobOrderPickOrdersLoading, setCompletedJobOrderPickOrdersLoading] = useState(false);
// 修改:详情视图状态
// 修改:详情视图状态
const [selectedJobOrderPickOrder, setSelectedJobOrderPickOrder] = useState<CompletedJobOrderPickOrder | null>(null);
const [showDetailView, setShowDetailView] = useState(false);
const [detailLotData, setDetailLotData] = useState<LotDetail[]>([]);
const [detailLotDataLoading, setDetailLotDataLoading] = useState(false);
// 修改:搜索状态
// 修改:搜索状态
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]);
// 修改:分页状态
// 修改:分页状态
const [paginationController, setPaginationController] = useState({
pageNum: 0,
pageSize: 10,
@@ -127,7 +127,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
const formProps = useForm();
const errors = formProps.formState.errors;

// 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders
// 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders
const fetchCompletedJobOrderPickOrdersData = useCallback(async () => {
if (!currentUserId) return;
@@ -139,7 +139,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
setCompletedJobOrderPickOrders(completedJobOrderPickOrders);
setFilteredJobOrderPickOrders(completedJobOrderPickOrders);
console.log(" Fetched completed Job Order pick orders:", completedJobOrderPickOrders);
console.log(" Fetched completed Job Order pick orders:", completedJobOrderPickOrders);
} catch (error) {
console.error("❌ Error fetching completed Job Order pick orders:", error);
setCompletedJobOrderPickOrders([]);
@@ -149,7 +149,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
}
}, [currentUserId]);

// 新增:获取 lot 详情数据
// 新增:获取 lot 详情数据
const fetchLotDetailsData = useCallback(async (pickOrderId: number) => {
setDetailLotDataLoading(true);
try {
@@ -158,7 +158,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
const lotDetails = await fetchCompletedJobOrderPickOrderLotDetails(pickOrderId);
setDetailLotData(lotDetails);
console.log(" Fetched lot details:", lotDetails);
console.log(" Fetched lot details:", lotDetails);
} catch (error) {
console.error("❌ Error fetching lot details:", error);
setDetailLotData([]);
@@ -167,14 +167,14 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
}
}, []);

// 修改:初始化时获取数据
// 修改:初始化时获取数据
useEffect(() => {
if (currentUserId) {
fetchCompletedJobOrderPickOrdersData();
}
}, [currentUserId, fetchCompletedJobOrderPickOrdersData]);

// 修改:搜索功能
// 修改:搜索功能
const handleSearch = useCallback((query: Record<string, any>) => {
setSearchQuery({ ...query });
console.log("Search query:", query);
@@ -196,13 +196,13 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
console.log("Filtered Job Order pick orders count:", filtered.length);
}, [completedJobOrderPickOrders]);

// 修改:重置搜索
// 修改:重置搜索
const handleSearchReset = useCallback(() => {
setSearchQuery({});
setFilteredJobOrderPickOrders(completedJobOrderPickOrders);
}, [completedJobOrderPickOrders]);

// 修改:分页功能
// 修改:分页功能
const handlePageChange = useCallback((event: unknown, newPage: number) => {
setPaginationController(prev => ({
...prev,
@@ -218,14 +218,14 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
});
}, []);

// 修改:分页数据
// 修改:分页数据
const paginatedData = useMemo(() => {
const startIndex = paginationController.pageNum * paginationController.pageSize;
const endIndex = startIndex + paginationController.pageSize;
return filteredJobOrderPickOrders.slice(startIndex, endIndex);
}, [filteredJobOrderPickOrders, paginationController]);

// 修改:搜索条件
// 修改:搜索条件
const searchCriteria: Criterion<any>[] = [
{
label: t("Pick Order Code"),
@@ -244,34 +244,34 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
}
];

// 修改:详情点击处理
// 修改:详情点击处理
const handleDetailClick = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => {
setSelectedJobOrderPickOrder(jobOrderPickOrder);
setShowDetailView(true);
// 获取 lot 详情数据
// 获取 lot 详情数据
await fetchLotDetailsData(jobOrderPickOrder.pickOrderId);
// 触发打印按钮状态更新 - 基于详情数据
// 触发打印按钮状态更新 - 基于详情数据
const allCompleted = jobOrderPickOrder.secondScanCompleted;
// 发送事件,包含标签页信息
// 发送事件,包含标签页信息
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: allCompleted,
tabIndex: 2 // 明确指定这是来自标签页 2 的事件
tabIndex: 2 // 明确指定这是来自标签页 2 的事件
}
}));
}, [fetchLotDetailsData]);

// 修改:返回列表视图
// 修改:返回列表视图
const handleBackToList = useCallback(() => {
setShowDetailView(false);
setSelectedJobOrderPickOrder(null);
setDetailLotData([]);
// 返回列表时禁用打印按钮
// 返回列表时禁用打印按钮
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
@@ -280,7 +280,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
}));
}, []);

// 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息
// 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息
if (showDetailView && selectedJobOrderPickOrder) {
return (
<FormProvider {...formProps}>
@@ -322,7 +322,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
</CardContent>
</Card>

{/* 修改:Lot 详情表格 - 添加复选框列 */}
{/* 修改:Lot 详情表格 - 添加复选框列 */}
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
@@ -353,7 +353,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
<TableBody>
{detailLotData.length === 0 ? (
<TableRow>
<TableCell colSpan={10} align="center"> {/* 恢复原来的 colSpan */}
<TableCell colSpan={10} align="center"> {/* 恢复原来的 colSpan */}
<Typography variant="body2" color="text.secondary">
{t("No lot details available")}
</Typography>
@@ -382,7 +382,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
<TableCell align="right">
{lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc})
</TableCell>
{/* 修改:Processing Status 使用复选框 */}
{/* 修改:Processing Status 使用复选框 */}
<TableCell align="center">
<Box sx={{
display: 'flex',
@@ -409,7 +409,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
/>
</Box>
</TableCell>
{/* 修改:Second Scan Status 使用复选框 */}
{/* 修改:Second Scan Status 使用复选框 */}
<TableCell align="center">
<Box sx={{
display: 'flex',
@@ -450,7 +450,7 @@ const FInishedJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
);
}

// 修改:默认列表视图
// 修改:默认列表视图
return (
<FormProvider {...formProps}>
<Box>


+ 90
- 90
src/components/Jodetail/JobPickExecution.tsx View File

@@ -36,7 +36,7 @@ import {
checkAndCompletePickOrderByConsoCode,
confirmLotSubstitution
} from "@/app/api/pickOrder/actions";
// 修改:使用 Job Order API
// 修改:使用 Job Order API
import {
fetchJobOrderLotsHierarchical,
fetchUnassignedJobOrderPickOrders,
@@ -62,7 +62,7 @@ interface Props {
filterArgs: Record<string, any>;
}

// QR Code Modal Component (from GoodPickExecution)
// QR Code Modal Component (from GoodPickExecution)
const QrCodeModal: React.FC<{
open: boolean;
onClose: () => void;
@@ -108,7 +108,7 @@ const QrCodeModal: React.FC<{
setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
if (stockInLineInfo.lotNo === lot.lotNo) {
console.log(` QR Code verified for lot: ${lot.lotNo}`);
console.log(` QR Code verified for lot: ${lot.lotNo}`);
setQrScanSuccess(true);
onQrCodeSubmit(lot.lotNo);
onClose();
@@ -300,7 +300,7 @@ const QrCodeModal: React.FC<{

{qrScanSuccess && (
<Typography variant="caption" color="success" display="block">
{t("Verified successfully!")}
{t("Verified successfully!")}
</Typography>
)}
</Box>
@@ -323,13 +323,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const currentUserId = session?.id ? parseInt(session.id) : undefined;
// 修改:使用 Job Order 数据结构
// 修改:使用 Job Order 数据结构
const [jobOrderData, setJobOrderData] = useState<any>(null);
const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
const [combinedDataLoading, setCombinedDataLoading] = useState(false);
const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
// 添加未分配订单状态
// 添加未分配订单状态
const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
@@ -359,23 +359,23 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const errors = formProps.formState.errors;
const [isSubmittingAll, setIsSubmittingAll] = useState<boolean>(false);

// Add QR modal states
// Add QR modal states
const [qrModalOpen, setQrModalOpen] = useState(false);
const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);

// Add GoodPickExecutionForm states
// Add GoodPickExecutionForm states
const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
const [fgPickOrders, setFgPickOrders] = useState<FGPickOrderResponse[]>([]);
const [fgPickOrdersLoading, setFgPickOrdersLoading] = useState(false);

// Add these missing state variables
// Add these missing state variables
const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);

// 修改:加载未分配的 Job Order 订单
// 修改:加载未分配的 Job Order 订单
const loadUnassignedOrders = useCallback(async () => {
setIsLoadingUnassigned(true);
try {
@@ -388,7 +388,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, []);

// 修改:分配订单给当前用户
// 修改:分配订单给当前用户
const handleAssignOrder = useCallback(async (pickOrderId: number) => {
if (!currentUserId) {
console.error("Missing user id in session");
@@ -398,7 +398,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
try {
const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
if (result.message === "Successfully assigned") {
console.log(" Successfully assigned pick order");
console.log(" Successfully assigned pick order");
// 刷新数据
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
// 重新加载未分配订单列表
@@ -437,7 +437,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const allFgPickOrders = fgPickOrdersResults.flat();
setFgPickOrders(allFgPickOrders);
console.log(" Fetched FG pick orders:", allFgPickOrders);
console.log(" Fetched FG pick orders:", allFgPickOrders);
} catch (error) {
console.error("❌ Error fetching FG pick orders:", error);
setFgPickOrders([]);
@@ -452,13 +452,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [combinedLotData, fetchFgPickOrdersData]);

// Handle QR code button click
// Handle QR code button click
const handleQrCodeClick = (pickOrderId: number) => {
console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
// TODO: Implement QR code functionality
};

// 修改:使用 Job Order API 获取数据
// 修改:使用 Job Order API 获取数据
const fetchJobOrderData = useCallback(async (userId?: number) => {
setCombinedDataLoading(true);
try {
@@ -479,13 +479,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
tabIndex: 0
}
}));
// 使用 Job Order API
// 使用 Job Order API
const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse);
console.log(" Job Order data:", jobOrderData);
console.log(" Job Order data:", jobOrderData);
setJobOrderData(jobOrderData);
// Transform hierarchical data to flat structure for the table
// Transform hierarchical data to flat structure for the table
const flatLotData: any[] = [];
if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) {
@@ -541,7 +541,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
});
}
console.log(" Transformed flat lot data:", flatLotData);
console.log(" Transformed flat lot data:", flatLotData);
setCombinedLotData(flatLotData);
setOriginalCombinedData(flatLotData);
const hasData = flatLotData.length > 0;
@@ -551,16 +551,16 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
tabIndex: 0
}
}));
// 计算完成状态并发送事件
// 计算完成状态并发送事件
const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
lot.processingStatus === 'completed'
);
// 发送完成状态事件,包含标签页信息
// 发送完成状态事件,包含标签页信息
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: allCompleted,
tabIndex: 0 // 明确指定这是来自标签页 0 的事件
tabIndex: 0 // 明确指定这是来自标签页 0 的事件
}
}));
@@ -570,7 +570,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
setCombinedLotData([]);
setOriginalCombinedData([]);
// 如果加载失败,禁用打印按钮
// 如果加载失败,禁用打印按钮
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
@@ -582,10 +582,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [currentUserId]);

// 修改:初始化时加载数据
// 修改:初始化时加载数据
useEffect(() => {
if (session && currentUserId && !initializationRef.current) {
console.log(" Session loaded, initializing job order...");
console.log(" Session loaded, initializing job order...");
initializationRef.current = true;
// 加载 Job Order 数据
@@ -595,7 +595,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]);

// Add event listener for manual assignment
// Add event listener for manual assignment
useEffect(() => {
const handlePickOrderAssigned = () => {
console.log("🔄 Pick order assigned event received, refreshing data...");
@@ -609,11 +609,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
};
}, [fetchJobOrderData]);

// Handle QR code submission for matched lot (external scanning)
// Handle QR code submission for matched lot (external scanning)
const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
console.log(` Processing QR Code for lot: ${lotNo}`);
console.log(` Processing QR Code for lot: ${lotNo}`);
// Use current data without refreshing to avoid infinite loop
// Use current data without refreshing to avoid infinite loop
const currentLotData = combinedLotData;
console.log(`🔍 Available lots:`, currentLotData.map(lot => lot.lotNo));
@@ -631,7 +631,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
return;
}
console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
console.log(` Found ${matchingLots.length} matching lots:`, matchingLots);
setQrScanError(false);
try {
@@ -706,13 +706,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}
// FIXED: Set refresh flag before refreshing data
// FIXED: Set refresh flag before refreshing data
setIsRefreshingData(true);
console.log("🔄 Refreshing data after QR code processing...");
await fetchJobOrderData();
if (successCount > 0) {
console.log(` QR Code processing completed: ${successCount} updated/created`);
console.log(` QR Code processing completed: ${successCount} updated/created`);
setQrScanSuccess(true);
setQrScanError(false);
setQrScanInput(''); // Clear input after successful processing
@@ -727,11 +727,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
setQrScanError(true);
setQrScanSuccess(false);
// Still refresh data even on error
// Still refresh data even on error
setIsRefreshingData(true);
await fetchJobOrderData();
} finally {
// Clear refresh flag after a short delay
// Clear refresh flag after a short delay
setTimeout(() => {
setIsRefreshingData(false);
}, 1000);
@@ -744,7 +744,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
setLotConfirmationOpen(true);
}, []);

// Add handleLotConfirmation function
// Add handleLotConfirmation function
const handleLotConfirmation = useCallback(async () => {
if (!expectedLotData || !scannedLotData || !selectedLotForQr) return;
setIsConfirmingLot(true);
@@ -767,7 +767,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log("Lot ID (fallback):", selectedLotForQr.lotId);
console.log("New Inventory Lot Line ID:", newLotLineId);

// Call confirmLotSubstitution to update the suggested lot
// Call confirmLotSubstitution to update the suggested lot
const substitutionResult = await confirmLotSubstitution({
pickOrderLineId: selectedLotForQr.pickOrderLineId,
stockOutLineId: selectedLotForQr.stockOutLineId,
@@ -775,52 +775,52 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
newInventoryLotLineId: newLotLineId
});
console.log(" Lot substitution result:", substitutionResult);
console.log(" Lot substitution result:", substitutionResult);
// Update stock out line status to 'checked' after substitution
// Update stock out line status to 'checked' after substitution
if(selectedLotForQr?.stockOutLineId){
await updateStockOutLineStatus({
id: selectedLotForQr.stockOutLineId,
status: 'checked',
qty: 0
});
console.log(" Stock out line status updated to 'checked'");
console.log(" Stock out line status updated to 'checked'");
}
// Close modal and clean up state BEFORE refreshing
// Close modal and clean up state BEFORE refreshing
setLotConfirmationOpen(false);
setExpectedLotData(null);
setScannedLotData(null);
setSelectedLotForQr(null);
// Clear QR processing state but DON'T clear processedQrCodes yet
// Clear QR processing state but DON'T clear processedQrCodes yet
setQrScanError(false);
setQrScanSuccess(true);
setQrScanInput('');
// Set refreshing flag to prevent QR processing during refresh
// Set refreshing flag to prevent QR processing during refresh
setIsRefreshingData(true);
// Refresh data to show updated lot
// Refresh data to show updated lot
console.log("🔄 Refreshing job order data...");
await fetchJobOrderData();
console.log(" Lot substitution confirmed and data refreshed");
console.log(" Lot substitution confirmed and data refreshed");
// Clear processed QR codes and flags immediately after refresh
// Clear processed QR codes and flags immediately after refresh
// This allows new QR codes to be processed right away
setTimeout(() => {
console.log(" Clearing processed QR codes and resuming scan");
console.log(" Clearing processed QR codes and resuming scan");
setProcessedQrCodes(new Set());
setLastProcessedQr('');
setQrScanSuccess(false);
setIsRefreshingData(false);
}, 500); // Reduced from 3000ms to 500ms - just enough for UI update
}, 500); // Reduced from 3000ms to 500ms - just enough for UI update
} catch (error) {
console.error("Error confirming lot substitution:", error);
setQrScanError(true);
setQrScanSuccess(false);
// Clear refresh flag on error
// Clear refresh flag on error
setIsRefreshingData(false);
} finally {
setIsConfirmingLot(false);
@@ -828,7 +828,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]);

const processOutsideQrCode = useCallback(async (latestQr: string) => {
// Don't process if confirmation modal is open
// Don't process if confirmation modal is open
if (lotConfirmationOpen) {
console.log("⏸️ Confirmation modal is open, skipping QR processing");
return;
@@ -857,7 +857,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
return;
}
// First, fetch stock in line info to get the lot number
// First, fetch stock in line info to get the lot number
let stockInLineInfo: any;
try {
stockInLineInfo = await fetchStockInLineInfo(qrData.stockInLineId);
@@ -923,7 +923,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
// 2) Check if the scanned lot matches exactly
if (scanned?.lotNo === expectedLot.lotNo) {
// Case 1: Exact match - process normally
console.log(` Exact lot match: ${scanned.lotNo}`);
console.log(` Exact lot match: ${scanned.lotNo}`);
await handleQrCodeSubmit(scanned.lotNo);
return;
}
@@ -931,7 +931,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
// Case 2: Same item, different lot - show confirmation modal
console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`);
// DON'T stop scanning - just pause QR processing by showing modal
// DON'T stop scanning - just pause QR processing by showing modal
setSelectedLotForQr(expectedLot);
handleLotMismatch(
{
@@ -962,10 +962,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [qrScanInput, handleQrCodeSubmit]);

// Handle QR code submission from modal (internal scanning)
// Handle QR code submission from modal (internal scanning)
const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
console.log(` QR Code verified for lot: ${lotNo}`);
console.log(` QR Code verified for lot: ${lotNo}`);
const requiredQty = selectedLotForQr.requiredQty;
const lotId = selectedLotForQr.lotId;
@@ -993,7 +993,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
...prev,
[lotKey]: requiredQty
}));
console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
}, 500);
// Refresh data
@@ -1006,7 +1006,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {

useEffect(() => {
// Add isManualScanning check
// Add isManualScanning check
if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData || lotConfirmationOpen) {
return;
}
@@ -1064,7 +1064,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
if (completionResponse.code === "SUCCESS" && completionResponse.entity?.hasCompletedOrders) {
console.log("Found completed pick orders, auto-assigning next...");
// 移除前端的自动分配逻辑,因为后端已经处理了
// 移除前端的自动分配逻辑,因为后端已经处理了
// await handleAutoAssignAndRelease(); // 删除这个函数
}
} catch (error) {
@@ -1072,7 +1072,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [currentUserId]);

// Handle submit pick quantity
// Handle submit pick quantity
const handleSubmitPickQty = useCallback(async (lot: any) => {
const lotKey = `${lot.pickOrderLineId}-${lot.lotId}`;
const newQty = pickQtyData[lotKey] || 0;
@@ -1116,14 +1116,14 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
});
}
// FIXED: Use the proper API function instead of direct fetch
// FIXED: Use the proper API function instead of direct fetch
if (newStatus === 'completed' && lot.pickOrderConsoCode) {
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
try {
// Use the imported API function instead of direct fetch
// Use the imported API function instead of direct fetch
const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
console.log(` Pick order completion check result:`, completionResponse);
console.log(` Pick order completion check result:`, completionResponse);
if (completionResponse.code === "SUCCESS") {
console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
@@ -1155,11 +1155,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
try {
// FIXED: Calculate cumulative quantity correctly
// FIXED: Calculate cumulative quantity correctly
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
// FIXED: Determine status based on cumulative quantity vs required quantity
// FIXED: Determine status based on cumulative quantity vs required quantity
let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
@@ -1182,7 +1182,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: newStatus,
qty: cumulativeQty // Use cumulative quantity
qty: cumulativeQty // Use cumulative quantity
});
if (submitQty > 0) {
@@ -1194,13 +1194,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
});
}
// Check if pick order is completed when lot status becomes 'completed'
// Check if pick order is completed when lot status becomes 'completed'
if (newStatus === 'completed' && lot.pickOrderConsoCode) {
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
console.log(` Lot ${lot.lotNo} completed, checking if pick order ${lot.pickOrderConsoCode} is complete...`);
try {
const completionResponse = await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
console.log(` Pick order completion check result:`, completionResponse);
console.log(` Pick order completion check result:`, completionResponse);
if (completionResponse.code === "SUCCESS") {
console.log(` Pick order ${lot.pickOrderConsoCode} completed successfully!`);
@@ -1239,7 +1239,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`);
try {
// Submit all items in parallel using Promise.all
// Submit all items in parallel using Promise.all
const submitPromises = scannedLots.map(async (lot) => {
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
const currentActualPickQty = lot.actualPickQty || 0;
@@ -1277,13 +1277,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
return { success: true, lotNo: lot.lotNo };
});
// Wait for all submissions to complete
// Wait for all submissions to complete
const results = await Promise.all(submitPromises);
const successCount = results.filter(r => r.success).length;
console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`);
console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`);
// Refresh data once after all submissions
// Refresh data once after all submissions
await fetchJobOrderData();
if (successCount > 0) {
@@ -1302,11 +1302,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]);

// Calculate scanned items count
// Calculate scanned items count
const scannedItemsCount = useMemo(() => {
return combinedLotData.filter(lot => lot.stockOutLineStatus === 'checked').length;
}, [combinedLotData]);
// Handle reject lot
// Handle reject lot
const handleRejectLot = useCallback(async (lot: any) => {
if (!lot.stockOutLineId) {
console.error("No stock out line found for this lot");
@@ -1332,7 +1332,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [fetchJobOrderData, checkAndAutoAssignNext]);

// Handle pick execution form
// Handle pick execution form
const handlePickExecutionForm = useCallback((lot: any) => {
console.log("=== Pick Execution Form ===");
console.log("Lot data:", lot);
@@ -1362,7 +1362,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log("Pick execution issue recorded:", result);
if (result && result.code === "SUCCESS") {
console.log(" Pick execution issue recorded successfully");
console.log(" Pick execution issue recorded successfully");
} else {
console.error("❌ Failed to record pick execution issue:", result);
}
@@ -1376,7 +1376,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [fetchJobOrderData]);

// Calculate remaining required quantity
// Calculate remaining required quantity
const calculateRemainingRequiredQty = useCallback((lot: any) => {
const requiredQty = lot.requiredQty || 0;
const stockOutLineQty = lot.stockOutLineQty || 0;
@@ -1457,7 +1457,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {

// Pagination data with sorting by routerIndex
const paginatedData = useMemo(() => {
// Sort by routerIndex first, then by other criteria
// Sort by routerIndex first, then by other criteria
const sortedData = [...combinedLotData].sort((a, b) => {
const aIndex = a.routerIndex || 0;
const bIndex = b.routerIndex || 0;
@@ -1481,7 +1481,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
return sortedData.slice(startIndex, endIndex);
}, [combinedLotData, paginationController]);

// Add these functions for manual scanning
// Add these functions for manual scanning
const handleStartScan = useCallback(() => {
console.log(" Starting manual QR scan...");
setIsManualScanning(true);
@@ -1517,7 +1517,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [combinedLotData.length, isManualScanning, handleStopScan]);

// Cleanup effect
// Cleanup effect
useEffect(() => {
return () => {
// Cleanup when component unmounts (e.g., when switching tabs)
@@ -1605,7 +1605,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
{t("Stop QR Scan")}
</Button>
)}
{/* ADD THIS: Submit All Scanned Button */}
{/* ADD THIS: Submit All Scanned Button */}
<Button
variant="contained"
color="success"
@@ -1750,7 +1750,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') ||
lot.stockOutLineStatus === 'completed' ||
lot.stockOutLineStatus === 'pending' // Disable when QR scan not passed
lot.stockOutLineStatus === 'pending' // Disable when QR scan not passed
}
sx={{
fontSize: '0.75rem',
@@ -1770,8 +1770,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') ||
lot.stockOutLineStatus === 'completed' || // Disable when finished
lot.stockOutLineStatus === 'pending' // Disable when QR scan not passed
lot.stockOutLineStatus === 'completed' || // Disable when finished
lot.stockOutLineStatus === 'pending' // Disable when QR scan not passed
}
sx={{
fontSize: '0.7rem',
@@ -1811,7 +1811,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
</Box>
</Stack>

{/* QR Code Modal */}
{/* QR Code Modal */}
{!lotConfirmationOpen && (
<QrCodeModal
open={qrModalOpen}
@@ -1826,7 +1826,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
onQrCodeSubmit={handleQrCodeSubmitFromModal}
/>
)}
{/* Add Lot Confirmation Modal */}
{/* Add Lot Confirmation Modal */}
{lotConfirmationOpen && expectedLotData && scannedLotData && (
<LotConfirmationModal
open={lotConfirmationOpen}
@@ -1843,7 +1843,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
isLoading={isConfirmingLot}
/>
)}
{/* Pick Execution Form Modal */}
{/* Pick Execution Form Modal */}
{pickExecutionFormOpen && selectedLotForExecutionForm && (
<GoodPickExecutionForm
open={pickExecutionFormOpen}
@@ -1859,13 +1859,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
itemCode: selectedLotForExecutionForm.itemCode,
itemName: selectedLotForExecutionForm.itemName,
pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
// Add missing required properties from GetPickOrderLineInfo interface
// Add missing required properties from GetPickOrderLineInfo interface
availableQty: selectedLotForExecutionForm.availableQty || 0,
requiredQty: selectedLotForExecutionForm.requiredQty || 0,
uomCode: selectedLotForExecutionForm.uomCode || '',
uomDesc: selectedLotForExecutionForm.uomDesc || '',
pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // Use pickedQty instead of actualPickQty
suggestedList: [] // Add required suggestedList property
pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // Use pickedQty instead of actualPickQty
suggestedList: [] // Add required suggestedList property
}}
pickOrderId={selectedLotForExecutionForm.pickOrderId}
pickOrderCreateDate={new Date()}


+ 20
- 20
src/components/Jodetail/JobPickExecutionForm.tsx View File

@@ -54,7 +54,7 @@ interface PickExecutionFormProps {
selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
pickOrderId?: number;
pickOrderCreateDate: any;
// Remove these props since we're not handling normal cases
// Remove these props since we're not handling normal cases
// onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
// selectedRowId?: number | null;
}
@@ -76,7 +76,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
selectedPickOrderLine,
pickOrderId,
pickOrderCreateDate,
// Remove these props
// Remove these props
// onNormalPickSubmit,
// selectedRowId,
}) => {
@@ -91,7 +91,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
return lot.availableQty || 0;
}, []);
const calculateRequiredQty = useCallback((lot: LotPickData) => {
// Use the original required quantity, not subtracting actualPickQty
// Use the original required quantity, not subtracting actualPickQty
// The actualPickQty in the form should be independent of the database value
return lot.requiredQty || 0;
}, []);
@@ -126,7 +126,7 @@ useEffect(() => {
}
};

// Initialize verified quantity to the received quantity (actualPickQty)
// Initialize verified quantity to the received quantity (actualPickQty)
const initialVerifiedQty = selectedLot.actualPickQty || 0;
setVerifiedQty(initialVerifiedQty);
@@ -155,14 +155,14 @@ useEffect(() => {
handledBy: undefined,
});
}
// 只在 open 状态改变时重新初始化,移除其他依赖
// 只在 open 状态改变时重新初始化,移除其他依赖
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);

const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Update verified quantity state when actualPickQty changes
// Update verified quantity state when actualPickQty changes
if (field === 'actualPickQty') {
setVerifiedQty(value);
}
@@ -173,7 +173,7 @@ useEffect(() => {
}
}, [errors]);

// Update form validation to require either missQty > 0 OR badItemQty > 0
// Update form validation to require either missQty > 0 OR badItemQty > 0
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
@@ -185,16 +185,16 @@ useEffect(() => {
newErrors.actualPickQty = t('Qty is required');
}
// 移除接收数量检查,因为在 JobPickExecution 阶段 receivedQty 总是 0
// 移除接收数量检查,因为在 JobPickExecution 阶段 receivedQty 总是 0
// if (verifiedQty > receivedQty) { ... } ← 删除
// 只检查总和是否等于需求数量
// 只检查总和是否等于需求数量
const totalQty = verifiedQty + badItemQty + missQty;
if (totalQty !== requiredQty) {
newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity');
}
// Require either missQty > 0 OR badItemQty > 0
// Require either missQty > 0 OR badItemQty > 0
const hasMissQty = formData.missQty && formData.missQty > 0;
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
@@ -213,7 +213,7 @@ useEffect(() => {

setLoading(true);
try {
// Use the verified quantity in the submission
// Use the verified quantity in the submission
const submissionData = {
...formData,
actualPickQty: verifiedQty,
@@ -249,11 +249,11 @@ useEffect(() => {
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
{t('Pick Execution Issue Form')} {/* Always show issue form title */}
{t('Pick Execution Issue Form')} {/* Always show issue form title */}
</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2 }}>
{/* Add instruction text */}
{/* Add instruction text */}
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
@@ -263,7 +263,7 @@ useEffect(() => {
</Box>
</Grid>
{/* Keep the existing form fields */}
{/* Keep the existing form fields */}
<Grid item xs={6}>
<TextField
fullWidth
@@ -297,7 +297,7 @@ useEffect(() => {
// handleInputChange('actualPickQty', newValue);
}}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // 使用原始接收数量
helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // 使用原始接收数量
variant="outlined"
/>
</Grid>
@@ -311,7 +311,7 @@ useEffect(() => {
onChange={(e) => {
const newMissQty = parseFloat(e.target.value) || 0;
handleInputChange('missQty', newMissQty);
// 不要自动修改其他字段
// 不要自动修改其他字段
}}
error={!!errors.missQty}
helperText={errors.missQty}
@@ -328,7 +328,7 @@ useEffect(() => {
onChange={(e) => {
const newBadItemQty = parseFloat(e.target.value) || 0;
handleInputChange('badItemQty', newBadItemQty);
// 不要自动修改其他字段
// 不要自动修改其他字段
}}
error={!!errors.badItemQty}
helperText={errors.badItemQty}
@@ -336,7 +336,7 @@ useEffect(() => {
/>
</Grid>
{/* Show issue description and handler fields when bad items > 0 */}
{/* Show issue description and handler fields when bad items > 0 */}
{(formData.badItemQty && formData.badItemQty > 0) ? (
<>
<Grid item xs={12}>
@@ -349,7 +349,7 @@ useEffect(() => {
value={formData.issueRemark || ''}
onChange={(e) => {
handleInputChange('issueRemark', e.target.value);
// Don't reset badItemQty when typing in issue remark
// Don't reset badItemQty when typing in issue remark
}}
error={!!errors.issueRemark}
helperText={errors.issueRemark}
@@ -365,7 +365,7 @@ useEffect(() => {
value={formData.handledBy ? formData.handledBy.toString() : ''}
onChange={(e) => {
handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined);
// Don't reset badItemQty when selecting handler
// Don't reset badItemQty when selecting handler
}}
label={t('handler')}
>


+ 73
- 73
src/components/Jodetail/JobPickExecutionsecondscan.tsx View File

@@ -24,7 +24,7 @@ import { useCallback, useEffect, useState, useRef, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useRouter } from "next/navigation";

// 修改:使用 Job Order API
// 修改:使用 Job Order API
import {
fetchCompletedJobOrderPickOrders,
fetchUnassignedJobOrderPickOrders,
@@ -54,7 +54,7 @@ interface Props {
filterArgs: Record<string, any>;
}

// QR Code Modal Component (from GoodPickExecution)
// QR Code Modal Component (from GoodPickExecution)
const QrCodeModal: React.FC<{
open: boolean;
onClose: () => void;
@@ -101,7 +101,7 @@ const QrCodeModal: React.FC<{
setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
if (stockInLineInfo.lotNo === lot.lotNo) {
console.log(` QR Code verified for lot: ${lot.lotNo}`);
console.log(` QR Code verified for lot: ${lot.lotNo}`);
setQrScanSuccess(true);
onQrCodeSubmit(lot.lotNo);
onClose();
@@ -293,7 +293,7 @@ const QrCodeModal: React.FC<{

{qrScanSuccess && (
<Typography variant="caption" color="success" display="block">
{t("Verified successfully!")}
{t("Verified successfully!")}
</Typography>
)}
</Box>
@@ -316,13 +316,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const currentUserId = session?.id ? parseInt(session.id) : undefined;
// 修改:使用 Job Order 数据结构
// 修改:使用 Job Order 数据结构
const [jobOrderData, setJobOrderData] = useState<any>(null);
const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
const [combinedDataLoading, setCombinedDataLoading] = useState(false);
const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
// 添加未分配订单状态
// 添加未分配订单状态
const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
@@ -348,20 +348,20 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const formProps = useForm();
const errors = formProps.formState.errors;

// Add QR modal states
// Add QR modal states
const [qrModalOpen, setQrModalOpen] = useState(false);
const [selectedLotForQr, setSelectedLotForQr] = useState<any | null>(null);

// Add GoodPickExecutionForm states
// Add GoodPickExecutionForm states
const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<any | null>(null);
// Add these missing state variables
// Add these missing state variables
const [isManualScanning, setIsManualScanning] = useState<boolean>(false);
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
// 修改:加载未分配的 Job Order 订单
// 修改:加载未分配的 Job Order 订单
const loadUnassignedOrders = useCallback(async () => {
setIsLoadingUnassigned(true);
try {
@@ -374,7 +374,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, []);

// 修改:分配订单给当前用户
// 修改:分配订单给当前用户
const handleAssignOrder = useCallback(async (pickOrderId: number) => {
if (!currentUserId) {
console.error("Missing user id in session");
@@ -384,7 +384,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
try {
const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
if (result.message === "Successfully assigned") {
console.log(" Successfully assigned pick order");
console.log(" Successfully assigned pick order");
// 刷新数据
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
// 重新加载未分配订单列表
@@ -400,13 +400,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}, [currentUserId, loadUnassignedOrders]);


// Handle QR code button click
// Handle QR code button click
const handleQrCodeClick = (pickOrderId: number) => {
console.log(`QR Code clicked for pick order ID: ${pickOrderId}`);
// TODO: Implement QR code functionality
};

// 修改:使用 Job Order API 获取数据
// 修改:使用 Job Order API 获取数据
const fetchJobOrderData = useCallback(async (userId?: number) => {
setCombinedDataLoading(true);
try {
@@ -427,15 +427,15 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
tabIndex: 1
}
}));
// 使用 Job Order API
// 使用 Job Order API
const jobOrderData = await fetchCompletedJobOrderPickOrders(userIdToUse);
console.log(" Job Order data:", jobOrderData);
console.log(" Pick Order Code from API:", jobOrderData.pickOrder?.code);
console.log(" Expected Pick Order Code: P-20251003-001");
console.log(" Job Order data:", jobOrderData);
console.log(" Pick Order Code from API:", jobOrderData.pickOrder?.code);
console.log(" Expected Pick Order Code: P-20251003-001");
setJobOrderData(jobOrderData);
// Transform hierarchical data to flat structure for the table
// Transform hierarchical data to flat structure for the table
const flatLotData: any[] = [];
if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) {
@@ -491,7 +491,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
});
}
console.log(" Transformed flat lot data:", flatLotData);
console.log(" Transformed flat lot data:", flatLotData);
setCombinedLotData(flatLotData);
setOriginalCombinedData(flatLotData);
const hasData = flatLotData.length > 0;
@@ -501,7 +501,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
tabIndex: 1
}
}));
// 计算完成状态并发送事件
// 计算完成状态并发送事件
const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
lot.processingStatus === 'completed'
);
@@ -511,11 +511,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
tabIndex: 1
}
}));
// 发送完成状态事件,包含标签页信息
// 发送完成状态事件,包含标签页信息
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: allCompleted,
tabIndex: 0 // 明确指定这是来自标签页 0 的事件
tabIndex: 0 // 明确指定这是来自标签页 0 的事件
}
}));
@@ -525,7 +525,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
setCombinedLotData([]);
setOriginalCombinedData([]);
// 如果加载失败,禁用打印按钮
// 如果加载失败,禁用打印按钮
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
@@ -550,7 +550,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`);
try {
// Submit all items in parallel using Promise.all
// Submit all items in parallel using Promise.all
const submitPromises = scannedLots.map(async (lot) => {
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
@@ -570,13 +570,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
return { success: result.code === "SUCCESS", itemCode: lot.itemCode };
});
// Wait for all submissions to complete
// Wait for all submissions to complete
const results = await Promise.all(submitPromises);
const successCount = results.filter(r => r.success).length;
console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`);
console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`);
// Refresh data once after all submissions
// Refresh data once after all submissions
await fetchJobOrderData();
if (successCount > 0) {
@@ -592,15 +592,15 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [combinedLotData, fetchJobOrderData]);

// Calculate scanned items count
// Calculate scanned items count
const scannedItemsCount = useMemo(() => {
return combinedLotData.filter(lot => lot.matchStatus === 'scanned').length;
}, [combinedLotData]);

// 修改:初始化时加载数据
// 修改:初始化时加载数据
useEffect(() => {
if (session && currentUserId && !initializationRef.current) {
console.log(" Session loaded, initializing job order...");
console.log(" Session loaded, initializing job order...");
initializationRef.current = true;
// 加载 Job Order 数据
@@ -610,7 +610,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [session, currentUserId, fetchJobOrderData, loadUnassignedOrders]);

// Add event listener for manual assignment
// Add event listener for manual assignment
useEffect(() => {
const handlePickOrderAssigned = () => {
console.log("🔄 Pick order assigned event received, refreshing data...");
@@ -624,11 +624,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
};
}, [fetchJobOrderData]);

// Handle QR code submission for matched lot (external scanning)
// Handle QR code submission for matched lot (external scanning)
const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
console.log(` Processing Second QR Code for lot: ${lotNo}`);
console.log(` Processing Second QR Code for lot: ${lotNo}`);
// Check if this lot was already processed recently
// Check if this lot was already processed recently
const lotKey = `${lotNo}_${Date.now()}`;
if (processedQrCodes.has(lotNo)) {
console.log(`⏭️ Lot ${lotNo} already processed, skipping...`);
@@ -652,26 +652,26 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
let successCount = 0;
for (const matchingLot of matchingLots) {
// Check if this specific item was already processed
// Check if this specific item was already processed
const itemKey = `${matchingLot.pickOrderId}_${matchingLot.itemId}`;
if (processedQrCodes.has(itemKey)) {
console.log(`⏭️ Item ${matchingLot.itemId} already processed, skipping...`);
continue;
}
// Use the new second scan API
// Use the new second scan API
const result = await updateSecondQrScanStatus(
matchingLot.pickOrderId,
matchingLot.itemId,
currentUserId || 0,
matchingLot.requiredQty || 1 // 传递实际的 required quantity
matchingLot.requiredQty || 1 // 传递实际的 required quantity
);
if (result.code === "SUCCESS") {
successCount++;
// Mark this item as processed
// Mark this item as processed
setProcessedQrCodes(prev => new Set(prev).add(itemKey));
console.log(` Second QR scan status updated for item ${matchingLot.itemId}`);
console.log(` Second QR scan status updated for item ${matchingLot.itemId}`);
} else {
console.error(`❌ Failed to update second QR scan status: ${result.message}`);
}
@@ -681,11 +681,11 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
setQrScanSuccess(true);
setQrScanError(false);
// Set refreshing flag briefly to prevent duplicate processing
// Set refreshing flag briefly to prevent duplicate processing
setIsRefreshingData(true);
await fetchJobOrderData(); // Refresh data
// Clear refresh flag and success message after a short delay
// Clear refresh flag and success message after a short delay
setTimeout(() => {
setQrScanSuccess(false);
setIsRefreshingData(false);
@@ -703,20 +703,20 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
useEffect(() => {
// Add isManualScanning and isRefreshingData checks
// Add isManualScanning and isRefreshingData checks
if (!isManualScanning || qrValues.length === 0 || combinedLotData.length === 0 || isRefreshingData) {
return;
}
const latestQr = qrValues[qrValues.length - 1];
// Check if this QR was already processed recently
// Check if this QR was already processed recently
if (processedQrCodes.has(latestQr) || lastProcessedQr === latestQr) {
console.log("⏭️ QR code already processed, skipping...");
return;
}
// Mark as processed
// Mark as processed
setProcessedQrCodes(prev => new Set(prev).add(latestQr));
setLastProcessedQr(latestQr);
@@ -752,7 +752,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [qrValues, combinedLotData, handleQrCodeSubmit, processedQrCodes, lastProcessedQr, isManualScanning, isRefreshingData]);

// ADD THIS: Cleanup effect
// ADD THIS: Cleanup effect
useEffect(() => {
return () => {
// Cleanup when component unmounts (e.g., when switching tabs)
@@ -769,10 +769,10 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [qrScanInput, handleQrCodeSubmit]);

// Handle QR code submission from modal (internal scanning)
// Handle QR code submission from modal (internal scanning)
const handleQrCodeSubmitFromModal = useCallback(async (lotNo: string) => {
if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
console.log(` QR Code verified for lot: ${lotNo}`);
console.log(` QR Code verified for lot: ${lotNo}`);
const requiredQty = selectedLotForQr.requiredQty;
const lotId = selectedLotForQr.lotId;
@@ -800,7 +800,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
...prev,
[lotKey]: requiredQty
}));
console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
}, 500);
// Refresh data
@@ -811,7 +811,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [selectedLotForQr, fetchJobOrderData]);

// Outside QR scanning - process QR codes from outside the page automatically
// Outside QR scanning - process QR codes from outside the page automatically

const handlePickQtyChange = useCallback((lotKey: string, value: number | string) => {
@@ -847,7 +847,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {

const handleSubmitPickQtyWithQty = useCallback(async (lot: any, submitQty: number) => {
try {
// Use the new second scan submit API
// Use the new second scan submit API
const result = await submitSecondScanQuantity(
lot.pickOrderId,
lot.itemId,
@@ -855,13 +855,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
qty: submitQty,
isMissing: false,
isBad: false,
reason: undefined // Fix TypeScript error
reason: undefined // Fix TypeScript error
}
);
if (result.code === "SUCCESS") {
console.log(` Second scan quantity submitted: ${submitQty}`);
console.log(` Second scan quantity submitted: ${submitQty}`);
await fetchJobOrderData(); // Refresh data
} else {
console.error(`❌ Failed to submit second scan quantity: ${result.message}`);
@@ -870,13 +870,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
console.error("Error submitting second scan quantity:", error);
}
}, [fetchJobOrderData]);
// Handle reject lot
// Handle reject lot

// Handle pick execution form
// Handle pick execution form
const handlePickExecutionForm = useCallback((lot: any) => {
console.log("=== Pick Execution Form ===");
console.log("Lot data:", lot);
console.log("lot.pickOrderCode:", lot.pickOrderCode); // 添加
console.log("lot.pickOrderCode:", lot.pickOrderCode); // 添加
console.log("lot.pickOrderId:", lot.pickOrderId);
if (!lot) {
@@ -904,8 +904,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
selectedLotForExecutionForm.itemId,
{
qty: data.actualPickQty, // verified qty
missQty: data.missQty || 0, // 添加:实际的 miss qty
badItemQty: data.badItemQty || 0, // 添加:实际的 bad item qty
missQty: data.missQty || 0, // 添加:实际的 miss qty
badItemQty: data.badItemQty || 0, // 添加:实际的 bad item qty
isMissing: data.missQty > 0,
isBad: data.badItemQty > 0,
reason: data.issueRemark || '',
@@ -916,7 +916,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log("Pick execution issue recorded:", result);
if (result && result.code === "SUCCESS") {
console.log(" Pick execution issue recorded successfully");
console.log(" Pick execution issue recorded successfully");
} else {
console.error("❌ Failed to record pick execution issue:", result);
}
@@ -930,7 +930,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [currentUserId, selectedLotForExecutionForm, fetchJobOrderData,]);

// Calculate remaining required quantity
// Calculate remaining required quantity
const calculateRemainingRequiredQty = useCallback((lot: any) => {
const requiredQty = lot.requiredQty || 0;
const stockOutLineQty = lot.stockOutLineQty || 0;
@@ -1011,7 +1011,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {

// Pagination data with sorting by routerIndex
const paginatedData = useMemo(() => {
// Sort by routerIndex first, then by other criteria
// Sort by routerIndex first, then by other criteria
const sortedData = [...combinedLotData].sort((a, b) => {
const aIndex = a.routerIndex || 0;
const bIndex = b.routerIndex || 0;
@@ -1035,7 +1035,7 @@ const paginatedData = useMemo(() => {
return sortedData.slice(startIndex, endIndex);
}, [combinedLotData, paginationController]);

// Add these functions for manual scanning
// Add these functions for manual scanning
const handleStartScan = useCallback(() => {
console.log(" Starting manual QR scan...");
setIsManualScanning(true);
@@ -1061,7 +1061,7 @@ const paginatedData = useMemo(() => {
}
}, [combinedLotData.length, isManualScanning, handleStopScan]);

// Cleanup effect
// Cleanup effect
useEffect(() => {
return () => {
// Cleanup when component unmounts (e.g., when switching tabs)
@@ -1151,7 +1151,7 @@ const paginatedData = useMemo(() => {
</Button>
)}
{/* ADD THIS: Submit All Scanned Button */}
{/* ADD THIS: Submit All Scanned Button */}
<Button
variant="contained"
color="success"
@@ -1259,12 +1259,12 @@ const paginatedData = useMemo(() => {
height: '100%'
}}>
<Checkbox
checked={true} // 改为 true
checked={true} // 改为 true
disabled={true}
readOnly={true}
size="large"
sx={{
color: 'success.main', // 固定为绿色
color: 'success.main', // 固定为绿色
'&.Mui-checked': {
color: 'success.main',
},
@@ -1296,7 +1296,7 @@ const paginatedData = useMemo(() => {
handleSubmitPickQtyWithQty(lot, submitQty);
}}
disabled={
// 修复:只有扫描过但未完成的才能提交
// 修复:只有扫描过但未完成的才能提交
lot.matchStatus !== 'scanned' || // 只有 scanned 状态才能提交
lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
@@ -1317,7 +1317,7 @@ const paginatedData = useMemo(() => {
size="small"
onClick={() => handlePickExecutionForm(lot)}
disabled={
// 修复:只有扫描过但未完成的才能报告问题
// 修复:只有扫描过但未完成的才能报告问题
lot.matchStatus !== 'scanned' || // 只有 scanned 状态才能报告问题
lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
@@ -1361,7 +1361,7 @@ const paginatedData = useMemo(() => {
</Box>
</Stack>

{/* QR Code Modal */}
{/* QR Code Modal */}
<QrCodeModal
open={qrModalOpen}
onClose={() => {
@@ -1375,7 +1375,7 @@ const paginatedData = useMemo(() => {
onQrCodeSubmit={handleQrCodeSubmitFromModal}
/>

{/* Pick Execution Form Modal */}
{/* Pick Execution Form Modal */}
{pickExecutionFormOpen && selectedLotForExecutionForm && (
<GoodPickExecutionForm
open={pickExecutionFormOpen}
@@ -1391,13 +1391,13 @@ const paginatedData = useMemo(() => {
itemCode: selectedLotForExecutionForm.itemCode,
itemName: selectedLotForExecutionForm.itemName,
pickOrderCode: selectedLotForExecutionForm.pickOrderCode,
// Add missing required properties from GetPickOrderLineInfo interface
// Add missing required properties from GetPickOrderLineInfo interface
availableQty: selectedLotForExecutionForm.availableQty || 0,
requiredQty: selectedLotForExecutionForm.requiredQty || 0,
uomCode: selectedLotForExecutionForm.uomCode || '',
uomDesc: selectedLotForExecutionForm.uomDesc || '',
pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // Use pickedQty instead of actualPickQty
suggestedList: [] // Add required suggestedList property
pickedQty: selectedLotForExecutionForm.actualPickQty || 0, // Use pickedQty instead of actualPickQty
suggestedList: [] // Add required suggestedList property
}}
pickOrderId={selectedLotForExecutionForm.pickOrderId}
pickOrderCreateDate={new Date()}


+ 21
- 21
src/components/Jodetail/JobmatchForm.tsx View File

@@ -54,7 +54,7 @@ interface PickExecutionFormProps {
selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
pickOrderId?: number;
pickOrderCreateDate: any;
// Remove these props since we're not handling normal cases
// Remove these props since we're not handling normal cases
// onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
// selectedRowId?: number | null;
}
@@ -76,7 +76,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
selectedPickOrderLine,
pickOrderId,
pickOrderCreateDate,
// Remove these props
// Remove these props
// onNormalPickSubmit,
// selectedRowId,
}) => {
@@ -93,7 +93,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
return Math.max(0, remainingQty);
}, []);
const calculateRequiredQty = useCallback((lot: LotPickData) => {
// Use the original required quantity, not subtracting actualPickQty
// Use the original required quantity, not subtracting actualPickQty
// The actualPickQty in the form should be independent of the database value
return lot.requiredQty || 0;
}, []);
@@ -128,7 +128,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
}
};

// Initialize verified quantity to the received quantity (actualPickQty)
// Initialize verified quantity to the received quantity (actualPickQty)
const initialVerifiedQty = selectedLot.actualPickQty || 0;
setVerifiedQty(initialVerifiedQty);
@@ -157,14 +157,14 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
handledBy: undefined,
});
}
// 修复:只在 open 状态改变时重新初始化,移除其他依赖
// 修复:只在 open 状态改变时重新初始化,移除其他依赖
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);

const handleInputChange = useCallback((field: keyof PickExecutionIssueData, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Update verified quantity state when actualPickQty changes
// Update verified quantity state when actualPickQty changes
if (field === 'actualPickQty') {
setVerifiedQty(value);
}
@@ -175,11 +175,11 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
}
}, [errors]);

// Update form validation to require either missQty > 0 OR badItemQty > 0
// Update form validation to require either missQty > 0 OR badItemQty > 0
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
// 使用原始的接收数量,而不是 formData 中的
// 使用原始的接收数量,而不是 formData 中的
const receivedQty = selectedLot?.actualPickQty || 0;
const requiredQty = selectedLot?.requiredQty || 0;
const badItemQty = formData.badItemQty || 0;
@@ -189,18 +189,18 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
newErrors.actualPickQty = t('Qty is required');
}
// 验证数量不能超过原始接收数量
// 验证数量不能超过原始接收数量
if (verifiedQty > receivedQty) {
newErrors.actualPickQty = t('Verified quantity cannot exceed received quantity');
}
// 只检查总和是否等于需求数量
// 只检查总和是否等于需求数量
const totalQty = verifiedQty + badItemQty + missQty;
if (totalQty !== requiredQty) {
newErrors.actualPickQty = t('Total (Verified + Bad + Missing) must equal Required quantity');
}
// Require either missQty > 0 OR badItemQty > 0
// Require either missQty > 0 OR badItemQty > 0
const hasMissQty = formData.missQty && formData.missQty > 0;
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
@@ -219,7 +219,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({

setLoading(true);
try {
// Use the verified quantity in the submission
// Use the verified quantity in the submission
const submissionData = {
...formData,
actualPickQty: verifiedQty,
@@ -257,11 +257,11 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
{t('Pick Execution Issue Form')} {/* Always show issue form title */}
{t('Pick Execution Issue Form')} {/* Always show issue form title */}
</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2 }}>
{/* Add instruction text */}
{/* Add instruction text */}
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
@@ -271,7 +271,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
</Box>
</Grid>
{/* Keep the existing form fields */}
{/* Keep the existing form fields */}
<Grid item xs={6}>
<TextField
fullWidth
@@ -306,7 +306,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
//handleInputChange('actualPickQty', newValue);
}}
error={!!errors.actualPickQty}
helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // 使用原始接收数量
helperText={errors.actualPickQty || `${t('Max')}: ${selectedLot?.actualPickQty || 0}`} // 使用原始接收数量
variant="outlined"
/>
</Grid>
@@ -320,7 +320,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
onChange={(e) => {
const newMissQty = parseFloat(e.target.value) || 0;
handleInputChange('missQty', newMissQty);
// 不要自动修改其他字段
// 不要自动修改其他字段
}}
error={!!errors.missQty}
helperText={errors.missQty}
@@ -337,7 +337,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
onChange={(e) => {
const newBadItemQty = parseFloat(e.target.value) || 0;
handleInputChange('badItemQty', newBadItemQty);
// 不要自动修改其他字段
// 不要自动修改其他字段
}}
error={!!errors.badItemQty}
helperText={errors.badItemQty}
@@ -345,7 +345,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
/>
</Grid>
{/* Show issue description and handler fields when bad items > 0 */}
{/* Show issue description and handler fields when bad items > 0 */}
{(formData.badItemQty && formData.badItemQty > 0) ? (
<>
<Grid item xs={12}>
@@ -358,7 +358,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
value={formData.issueRemark || ''}
onChange={(e) => {
handleInputChange('issueRemark', e.target.value);
// Don't reset badItemQty when typing in issue remark
// Don't reset badItemQty when typing in issue remark
}}
error={!!errors.issueRemark}
helperText={errors.issueRemark}
@@ -374,7 +374,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
value={formData.handledBy ? formData.handledBy.toString() : ''}
onChange={(e) => {
handleInputChange('handledBy', e.target.value ? parseInt(e.target.value) : undefined);
// Don't reset badItemQty when selecting handler
// Don't reset badItemQty when selecting handler
}}
label={t('handler')}
>


+ 10
- 10
src/components/Jodetail/JodetailSearch.tsx View File

@@ -106,7 +106,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
const handleCompletionStatusChange = (event: CustomEvent) => {
const { allLotsCompleted, tabIndex: eventTabIndex } = event.detail;
// 修复:根据标签页和事件来源决定是否更新打印按钮状态
// 修复:根据标签页和事件来源决定是否更新打印按钮状态
if (eventTabIndex === undefined || eventTabIndex === tabIndex) {
setPrintButtonsEnabled(allLotsCompleted);
console.log(`Print buttons enabled for tab ${tabIndex}:`, allLotsCompleted);
@@ -118,9 +118,9 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
return () => {
window.removeEventListener('pickOrderCompletionStatus', handleCompletionStatusChange as EventListener);
};
}, [tabIndex]); // 添加 tabIndex 依赖
}, [tabIndex]); // 添加 tabIndex 依赖

// 新增:处理标签页切换时的打印按钮状态重置
// 新增:处理标签页切换时的打印按钮状态重置
useEffect(() => {
// 当切换到标签页 2 (GoodPickExecutionRecord) 时,重置打印按钮状态
if (tabIndex === 2) {
@@ -141,14 +141,14 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
const res = await autoAssignAndReleasePickOrderByStore(currentUserId, storeId);
console.log("Assign by store result:", res);
// Handle different response codes
// Handle different response codes
if (res.code === "SUCCESS") {
console.log(" Successfully assigned pick order to store", storeId);
// Trigger refresh to show newly assigned data
console.log(" Successfully assigned pick order to store", storeId);
// Trigger refresh to show newly assigned data
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
} else if (res.code === "USER_BUSY") {
console.warn("⚠️ User already has pick orders in progress:", res.message);
// Show warning but still refresh to show existing orders
// Show warning but still refresh to show existing orders
alert(`Warning: ${res.message}`);
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
} else if (res.code === "NO_ORDERS") {
@@ -165,7 +165,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
setIsAssigning(false);
}
};
// Manual assignment handler - uses the action function
// Manual assignment handler - uses the action function
const loadUnassignedOrders = useCallback(async () => {
setIsLoadingUnassigned(true);
try {
@@ -189,7 +189,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
try {
const result = await assignJobOrderPickOrder(pickOrderId, currentUserId);
if (result.message === "Successfully assigned") {
console.log(" Successfully assigned pick order");
console.log(" Successfully assigned pick order");
// 刷新数据
window.dispatchEvent(new CustomEvent('pickOrderAssigned'));
// 重新加载未分配订单列表
@@ -448,7 +448,7 @@ const hasAnyAssignedData = hasDataTab0 || hasDataTab1;
</Stack>
</Box>

{/* Tabs section - Move the click handler here */}
{/* Tabs section - Move the click handler here */}
<Box sx={{
borderBottom: '1px solid #e0e0e0'
}}>


+ 32
- 32
src/components/Jodetail/completeJobOrderRecord.tsx View File

@@ -49,7 +49,7 @@ interface Props {
filterArgs: Record<string, any>;
}

// 修改:已完成的 Job Order Pick Order 接口
// 修改:已完成的 Job Order Pick Order 接口
interface CompletedJobOrderPickOrder {
id: number;
pickOrderId: number;
@@ -70,7 +70,7 @@ interface CompletedJobOrderPickOrder {
completedItems: number;
}

// 新增:Lot 详情接口
// 新增:Lot 详情接口
interface LotDetail {
lotId: number;
lotNo: string;
@@ -106,21 +106,21 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
const currentUserId = session?.id ? parseInt(session.id) : undefined;
// 修改:已完成 Job Order Pick Orders 状态
// 修改:已完成 Job Order Pick Orders 状态
const [completedJobOrderPickOrders, setCompletedJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]);
const [completedJobOrderPickOrdersLoading, setCompletedJobOrderPickOrdersLoading] = useState(false);
// 修改:详情视图状态
// 修改:详情视图状态
const [selectedJobOrderPickOrder, setSelectedJobOrderPickOrder] = useState<CompletedJobOrderPickOrder | null>(null);
const [showDetailView, setShowDetailView] = useState(false);
const [detailLotData, setDetailLotData] = useState<LotDetail[]>([]);
const [detailLotDataLoading, setDetailLotDataLoading] = useState(false);
// 修改:搜索状态
// 修改:搜索状态
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});
const [filteredJobOrderPickOrders, setFilteredJobOrderPickOrders] = useState<CompletedJobOrderPickOrder[]>([]);
// 修改:分页状态
// 修改:分页状态
const [paginationController, setPaginationController] = useState({
pageNum: 0,
pageSize: 10,
@@ -129,7 +129,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
const formProps = useForm();
const errors = formProps.formState.errors;

// 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders(仅完成pick的)
// 修改:使用新的 Job Order API 获取已完成的 Job Order Pick Orders(仅完成pick的)
const fetchCompletedJobOrderPickOrdersData = useCallback(async () => {
if (!currentUserId) return;
@@ -139,12 +139,12 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
const completedJobOrderPickOrders = await fetchCompletedJobOrderPickOrdersrecords(currentUserId);
// Fix: Ensure the data is always an array
// Fix: Ensure the data is always an array
const safeData = Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : [];
setCompletedJobOrderPickOrders(safeData);
setFilteredJobOrderPickOrders(safeData);
console.log(" Fetched completed Job Order pick orders:", safeData);
console.log(" Fetched completed Job Order pick orders:", safeData);
} catch (error) {
console.error("❌ Error fetching completed Job Order pick orders:", error);
setCompletedJobOrderPickOrders([]);
@@ -154,7 +154,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
}
}, [currentUserId]);

// 新增:获取 lot 详情数据(使用新的API)
// 新增:获取 lot 详情数据(使用新的API)
const fetchLotDetailsData = useCallback(async (pickOrderId: number) => {
setDetailLotDataLoading(true);
try {
@@ -163,7 +163,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
const lotDetails = await fetchCompletedJobOrderPickOrderLotDetailsForCompletedPick(pickOrderId);
setDetailLotData(lotDetails);
console.log(" Fetched lot details:", lotDetails);
console.log(" Fetched lot details:", lotDetails);
} catch (error) {
console.error("❌ Error fetching lot details:", error);
setDetailLotData([]);
@@ -172,19 +172,19 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
}
}, []);

// 修改:初始化时获取数据
// 修改:初始化时获取数据
useEffect(() => {
if (currentUserId) {
fetchCompletedJobOrderPickOrdersData();
}
}, [currentUserId, fetchCompletedJobOrderPickOrdersData]);

// 修改:搜索功能
// 修改:搜索功能
const handleSearch = useCallback((query: Record<string, any>) => {
setSearchQuery({ ...query });
console.log("Search query:", query);
// Fix: Ensure completedJobOrderPickOrders is an array before filtering
// Fix: Ensure completedJobOrderPickOrders is an array before filtering
if (!Array.isArray(completedJobOrderPickOrders)) {
setFilteredJobOrderPickOrders([]);
return;
@@ -207,14 +207,14 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
console.log("Filtered Job Order pick orders count:", filtered.length);
}, [completedJobOrderPickOrders]);

// 修改:重置搜索
// 修改:重置搜索
const handleSearchReset = useCallback(() => {
setSearchQuery({});
// Fix: Ensure completedJobOrderPickOrders is an array before setting
// Fix: Ensure completedJobOrderPickOrders is an array before setting
setFilteredJobOrderPickOrders(Array.isArray(completedJobOrderPickOrders) ? completedJobOrderPickOrders : []);
}, [completedJobOrderPickOrders]);

// 修改:分页功能
// 修改:分页功能
const handlePageChange = useCallback((event: unknown, newPage: number) => {
setPaginationController(prev => ({
...prev,
@@ -230,9 +230,9 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
});
}, []);

// 修改:分页数据
// 修改:分页数据
const paginatedData = useMemo(() => {
// Fix: Ensure filteredJobOrderPickOrders is an array before calling slice
// Fix: Ensure filteredJobOrderPickOrders is an array before calling slice
if (!Array.isArray(filteredJobOrderPickOrders)) {
return [];
}
@@ -242,7 +242,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
return filteredJobOrderPickOrders.slice(startIndex, endIndex);
}, [filteredJobOrderPickOrders, paginationController]);

// 修改:搜索条件
// 修改:搜索条件
const searchCriteria: Criterion<any>[] = [
{
label: t("Pick Order Code"),
@@ -261,34 +261,34 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
}
];

// 修改:详情点击处理
// 修改:详情点击处理
const handleDetailClick = useCallback(async (jobOrderPickOrder: CompletedJobOrderPickOrder) => {
setSelectedJobOrderPickOrder(jobOrderPickOrder);
setShowDetailView(true);
// 获取 lot 详情数据(使用新的API)
// 获取 lot 详情数据(使用新的API)
await fetchLotDetailsData(jobOrderPickOrder.pickOrderId);
// 触发打印按钮状态更新 - 基于详情数据
// 触发打印按钮状态更新 - 基于详情数据
const allCompleted = jobOrderPickOrder.secondScanCompleted;
// 发送事件,包含标签页信息
// 发送事件,包含标签页信息
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: allCompleted,
tabIndex: 3 // 明确指定这是来自标签页 3 的事件
tabIndex: 3 // 明确指定这是来自标签页 3 的事件
}
}));
}, [fetchLotDetailsData]);

// 修改:返回列表视图
// 修改:返回列表视图
const handleBackToList = useCallback(() => {
setShowDetailView(false);
setSelectedJobOrderPickOrder(null);
setDetailLotData([]);
// 返回列表时禁用打印按钮
// 返回列表时禁用打印按钮
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
@@ -335,7 +335,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {

},[t, selectedJobOrderPickOrder]);

// 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息
// 修改:如果显示详情视图,渲染 Job Order 详情和 Lot 信息
if (showDetailView && selectedJobOrderPickOrder) {
return (
<FormProvider {...formProps}>
@@ -386,7 +386,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
</CardContent>
</Card>

{/* 修改:Lot 详情表格 - 添加复选框列 */}
{/* 修改:Lot 详情表格 - 添加复选框列 */}
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
@@ -446,7 +446,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
<TableCell align="right">
{lot.actualPickQty?.toLocaleString() || 0} ({lot.uomShortDesc})
</TableCell>
{/* 修改:Processing Status 使用复选框 */}
{/* 修改:Processing Status 使用复选框 */}
<TableCell align="center">
<Box sx={{
display: 'flex',
@@ -473,7 +473,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
/>
</Box>
</TableCell>
{/* 修改:Second Scan Status 使用复选框 */}
{/* 修改:Second Scan Status 使用复选框 */}
<TableCell align="center">
<Box sx={{
display: 'flex',
@@ -514,7 +514,7 @@ const CompleteJobOrderRecord: React.FC<Props> = ({ filterArgs }) => {
);
}

// 修改:默认列表视图
// 修改:默认列表视图
return (
<FormProvider {...formProps}>
<Box>


+ 66
- 66
src/components/PickOrderSearch/LotTable.tsx View File

@@ -24,7 +24,7 @@ import { GetPickOrderLineInfo, recordPickExecutionIssue } from "@/app/api/pickOr
import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
import { updateInventoryLotLineStatus } from "@/app/api/inventory/actions";
import { updateStockOutLineStatus } from "@/app/api/pickOrder/actions";
import { fetchStockInLineInfo } from "@/app/api/po/actions"; // Add this import
import { fetchStockInLineInfo } from "@/app/api/po/actions"; // Add this import
import PickExecutionForm from "./PickExecutionForm";
interface LotPickData {
id: number;
@@ -41,7 +41,7 @@ interface LotPickData {
outQty: number;
holdQty: number;
totalPickedByAllPickOrders: number;
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable' | 'rejected'; // 添加 'rejected'
lotAvailability: 'available' | 'insufficient_stock' | 'expired' | 'status_unavailable' | 'rejected'; // 添加 'rejected'
stockOutLineId?: number;
stockOutLineStatus?: string;
stockOutLineQty?: number;
@@ -56,7 +56,7 @@ interface PickQtyData {
interface LotTableProps {
lotData: LotPickData[];
selectedRowId: number | null;
selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string; pickOrderId: number }) | null; // 添加 pickOrderId
selectedRow: (GetPickOrderLineInfo & { pickOrderCode: string; pickOrderId: number }) | null; // 添加 pickOrderId
pickQtyData: PickQtyData;
selectedLotRowId: string | null;
selectedLotId: number | null;
@@ -77,7 +77,7 @@ interface LotTableProps {
onLotDataRefresh: () => Promise<void>;
}

// QR Code Modal Component
// QR Code Modal Component
const QrCodeModal: React.FC<{
open: boolean;
onClose: () => void;
@@ -88,53 +88,53 @@ const QrCodeModal: React.FC<{
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
const [manualInput, setManualInput] = useState<string>('');
const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({});
// Add state to track manual input submission
// Add state to track manual input submission
const [manualInputSubmitted, setManualInputSubmitted] = useState<boolean>(false);
const [manualInputError, setManualInputError] = useState<boolean>(false);
const [isProcessingQr, setIsProcessingQr] = useState<boolean>(false);
const [qrScanFailed, setQrScanFailed] = useState<boolean>(false);
const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
// Add state to track processed QR codes to prevent re-processing
// Add state to track processed QR codes to prevent re-processing
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
// Add state to store the scanned QR result
// Add state to store the scanned QR result
const [scannedQrResult, setScannedQrResult] = useState<string>('');

// Process scanned QR codes with new format
// Process scanned QR codes with new format
useEffect(() => {
if (qrValues.length > 0 && lot && !isProcessingQr && !qrScanSuccess) {
const latestQr = qrValues[qrValues.length - 1];
// Check if this QR code has already been processed
// Check if this QR code has already been processed
if (processedQrCodes.has(latestQr)) {
console.log("QR code already processed, skipping...");
return;
}
// Add to processed set immediately to prevent re-processing
// Add to processed set immediately to prevent re-processing
setProcessedQrCodes(prev => new Set(prev).add(latestQr));
try {
// Parse QR code as JSON
// Parse QR code as JSON
const qrData = JSON.parse(latestQr);
// Check if it has the expected structure
// Check if it has the expected structure
if (qrData.stockInLineId && qrData.itemId) {
setIsProcessingQr(true);
setQrScanFailed(false);
// Fetch stock in line info to get lotNo
// Fetch stock in line info to get lotNo
fetchStockInLineInfo(qrData.stockInLineId)
.then((stockInLineInfo) => {
console.log("Stock in line info:", stockInLineInfo);
// Store the scanned result for display
// Store the scanned result for display
setScannedQrResult(stockInLineInfo.lotNo || 'Unknown lot number');
// Compare lotNo from API with expected lotNo
// Compare lotNo from API with expected lotNo
if (stockInLineInfo.lotNo === lot.lotNo) {
console.log(` QR Code verified for lot: ${lot.lotNo}`);
console.log(` QR Code verified for lot: ${lot.lotNo}`);
setQrScanSuccess(true);
onQrCodeSubmit(lot.lotNo);
onClose();
@@ -144,7 +144,7 @@ const QrCodeModal: React.FC<{
setQrScanFailed(true);
setManualInputError(true);
setManualInputSubmitted(true);
// DON'T stop scanning - allow new QR codes to be processed
// DON'T stop scanning - allow new QR codes to be processed
}
})
.catch((error) => {
@@ -153,16 +153,16 @@ const QrCodeModal: React.FC<{
setQrScanFailed(true);
setManualInputError(true);
setManualInputSubmitted(true);
// DON'T stop scanning - allow new QR codes to be processed
// DON'T stop scanning - allow new QR codes to be processed
})
.finally(() => {
setIsProcessingQr(false);
});
} else {
// Fallback to old format (direct lotNo comparison)
// Fallback to old format (direct lotNo comparison)
const qrContent = latestQr.replace(/[{}]/g, '');
// Store the scanned result for display
// Store the scanned result for display
setScannedQrResult(qrContent);
if (qrContent === lot.lotNo) {
@@ -174,15 +174,15 @@ const QrCodeModal: React.FC<{
setQrScanFailed(true);
setManualInputError(true);
setManualInputSubmitted(true);
// DON'T stop scanning - allow new QR codes to be processed
// DON'T stop scanning - allow new QR codes to be processed
}
}
} catch (error) {
// If JSON parsing fails, fallback to old format
// If JSON parsing fails, fallback to old format
console.log("QR code is not JSON format, trying direct comparison");
const qrContent = latestQr.replace(/[{}]/g, '');
// Store the scanned result for display
// Store the scanned result for display
setScannedQrResult(qrContent);
if (qrContent === lot.lotNo) {
@@ -194,13 +194,13 @@ const QrCodeModal: React.FC<{
setQrScanFailed(true);
setManualInputError(true);
setManualInputSubmitted(true);
// DON'T stop scanning - allow new QR codes to be processed
// DON'T stop scanning - allow new QR codes to be processed
}
}
}
}, [qrValues, lot, onQrCodeSubmit, onClose, resetScan, isProcessingQr, qrScanSuccess, processedQrCodes, stopScan]);

// Clear states when modal opens or lot changes
// Clear states when modal opens or lot changes
useEffect(() => {
if (open) {
setManualInput('');
@@ -209,8 +209,8 @@ const QrCodeModal: React.FC<{
setIsProcessingQr(false);
setQrScanFailed(false);
setQrScanSuccess(false);
setScannedQrResult(''); // Clear scanned result
// Clear processed QR codes when modal opens
setScannedQrResult(''); // Clear scanned result
// Clear processed QR codes when modal opens
setProcessedQrCodes(new Set());
}
}, [open]);
@@ -223,13 +223,13 @@ const QrCodeModal: React.FC<{
setIsProcessingQr(false);
setQrScanFailed(false);
setQrScanSuccess(false);
setScannedQrResult(''); // Clear scanned result
// Clear processed QR codes when lot changes
setScannedQrResult(''); // Clear scanned result
// Clear processed QR codes when lot changes
setProcessedQrCodes(new Set());
}
}, [lot]);

// Auto-submit manual input when it matches (but only if QR scan hasn't failed)
// Auto-submit manual input when it matches (but only if QR scan hasn't failed)
useEffect(() => {
if (manualInput.trim() === lot?.lotNo && manualInput.trim() !== '' && !qrScanFailed && !qrScanSuccess) {
console.log('🔄 Auto-submitting manual input:', manualInput.trim());
@@ -247,7 +247,7 @@ const QrCodeModal: React.FC<{
}
}, [manualInput, lot, onQrCodeSubmit, onClose, qrScanFailed, qrScanSuccess]);

// Add the missing handleManualSubmit function
// Add the missing handleManualSubmit function
const handleManualSubmit = () => {
if (manualInput.trim() === lot?.lotNo) {
setQrScanSuccess(true);
@@ -261,7 +261,7 @@ const QrCodeModal: React.FC<{
}
};
// Add function to restart scanning after manual input error
// Add function to restart scanning after manual input error
const handleRestartScan = () => {
setQrScanFailed(false);
setManualInputError(false);
@@ -292,7 +292,7 @@ const QrCodeModal: React.FC<{
{t("QR Code Scan for Lot")}: {lot?.lotNo}
</Typography>
{/* Show processing status */}
{/* Show processing status */}
{isProcessingQr && (
<Box sx={{ mb: 2, p: 2, backgroundColor: '#e3f2fd', borderRadius: 1 }}>
<Typography variant="body2" color="primary">
@@ -312,7 +312,7 @@ const QrCodeModal: React.FC<{
value={manualInput}
onChange={(e) => {
setManualInput(e.target.value);
// Reset error states when user starts typing
// Reset error states when user starts typing
if (qrScanFailed || manualInputError) {
setQrScanFailed(false);
setManualInputError(false);
@@ -352,7 +352,7 @@ const QrCodeModal: React.FC<{

{qrScanSuccess && (
<Typography variant="caption" color="success" display="block">
{t("Verified successfully!")}
{t("Verified successfully!")}
</Typography>
)}
</Box>
@@ -395,10 +395,10 @@ const LotTable: React.FC<LotTableProps> = ({
const stockOutLineQty = lot.stockOutLineQty || 0;
return Math.max(0, requiredQty - stockOutLineQty);
}, []);
// Add QR scanner context
// Add QR scanner context
const { values: qrValues, isScanning, startScan, stopScan, resetScan } = useQrCodeScannerContext();
const [validationErrors, setValidationErrors] = useState<{[key: string]: string}>({});
// Add state for QR input modal
// Add state for QR input modal
const [qrModalOpen, setQrModalOpen] = useState(false);
const [selectedLotForQr, setSelectedLotForQr] = useState<LotPickData | null>(null);
const [manualQrInput, setManualQrInput] = useState<string>('');
@@ -409,7 +409,7 @@ const LotTable: React.FC<LotTableProps> = ({
pageSize: 10,
});

// 添加状态消息生成函数
// 添加状态消息生成函数
const getStatusMessage = useCallback((lot: LotPickData) => {

switch (lot.stockOutLineStatus?.toLowerCase()) {
@@ -483,45 +483,45 @@ const LotTable: React.FC<LotTableProps> = ({
return null;
}, [calculateRemainingAvailableQty, calculateRemainingRequiredQty, t]);

// Handle QR code submission
// Handle QR code submission
const handleQrCodeSubmit = useCallback(async (lotNo: string) => {
if (selectedLotForQr && selectedLotForQr.lotNo === lotNo) {
console.log(` QR Code verified for lot: ${lotNo}`);
console.log(` QR Code verified for lot: ${lotNo}`);
if (!selectedLotForQr.stockOutLineId) {
console.error("No stock out line ID found for this lot");
alert("No stock out line found for this lot. Please contact administrator.");
return;
}
// Store the required quantity before creating stock out line
// Store the required quantity before creating stock out line
const requiredQty = selectedLotForQr.requiredQty;
const lotId = selectedLotForQr.lotId;
try {
// Update stock out line status to 'checked' (QR scan completed)
// Update stock out line status to 'checked' (QR scan completed)
const stockOutLineUpdate = await updateStockOutLineStatus({
id: selectedLotForQr.stockOutLineId,
status: 'checked',
qty: selectedLotForQr.stockOutLineQty || 0
});
console.log(" Stock out line updated to 'checked':", stockOutLineUpdate);
console.log(" Stock out line updated to 'checked':", stockOutLineUpdate);
// Close modal
// Close modal
setQrModalOpen(false);
setSelectedLotForQr(null);
if (onLotDataRefresh) {
await onLotDataRefresh();
}
// Set pick quantity AFTER stock out line update is complete
// Set pick quantity AFTER stock out line update is complete
if (selectedRowId) {
// Add a small delay to ensure the data refresh is complete
setTimeout(() => {
onPickQtyChange(selectedRowId, lotId, requiredQty);
console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
console.log(` Auto-set pick quantity to ${requiredQty} for lot ${lotNo}`);
}, 500); // 500ms delay to ensure refresh is complete
}
// Show success message
// Show success message
console.log("Stock out line updated successfully!");
} catch (error) {
@@ -529,17 +529,17 @@ const LotTable: React.FC<LotTableProps> = ({
alert("Failed to update lot status. Please try again.");
}
} else {
// Handle case where lot numbers don't match
// Handle case where lot numbers don't match
console.error("QR scan mismatch:", { scanned: lotNo, expected: selectedLotForQr?.lotNo });
alert(`QR scan mismatch! Expected: ${selectedLotForQr?.lotNo}, Scanned: ${lotNo}`);
}
}, [selectedLotForQr, selectedRowId, onPickQtyChange]);

// 添加 PickExecutionForm 相关的状态
// 添加 PickExecutionForm 相关的状态
const [pickExecutionFormOpen, setPickExecutionFormOpen] = useState(false);
const [selectedLotForExecutionForm, setSelectedLotForExecutionForm] = useState<LotPickData | null>(null);

// 添加处理函数
// 添加处理函数
const handlePickExecutionForm = useCallback((lot: LotPickData) => {
console.log("=== Pick Execution Form ===");
console.log("Lot data:", lot);
@@ -561,12 +561,12 @@ const LotTable: React.FC<LotTableProps> = ({
try {
console.log("Pick execution form submitted:", data);
// 调用 API 提交数据
// 调用 API 提交数据
const result = await recordPickExecutionIssue(data);
console.log("Pick execution issue recorded:", result);
if (result && result.code === "SUCCESS") {
console.log(" Pick execution issue recorded successfully");
console.log(" Pick execution issue recorded successfully");
} else {
console.error("❌ Failed to record pick execution issue:", result);
}
@@ -574,7 +574,7 @@ const LotTable: React.FC<LotTableProps> = ({
setPickExecutionFormOpen(false);
setSelectedLotForExecutionForm(null);
// 刷新数据
// 刷新数据
if (onDataRefresh) {
await onDataRefresh();
}
@@ -636,10 +636,10 @@ const LotTable: React.FC<LotTableProps> = ({
<Checkbox
checked={selectedLotRowId === `row_${index}`}
onChange={() => onLotSelection(`row_${index}`, lot.lotId)}
// 禁用 rejected、expired 和 status_unavailable 的批次
// 禁用 rejected、expired 和 status_unavailable 的批次
disabled={lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected'} // 添加 rejected
lot.lotAvailability === 'rejected'} // 添加 rejected
value={`row_${index}`}
name="lot-selection"
/>
@@ -659,7 +659,7 @@ const LotTable: React.FC<LotTableProps> = ({
<Typography variant="caption" color="error" display="block">
({lot.lotAvailability === 'expired' ? 'Expired' :
lot.lotAvailability === 'insufficient_stock' ? 'Insufficient' :
lot.lotAvailability === 'rejected' ? 'Rejected' : // 添加 rejected 显示
lot.lotAvailability === 'rejected' ? 'Rejected' : // 添加 rejected 显示
'Unavailable'})
</Typography>
)} */}
@@ -718,13 +718,13 @@ const LotTable: React.FC<LotTableProps> = ({
direction="row"
spacing={1}
alignItems="center"
justifyContent="center" // 添加水平居中
justifyContent="center" // 添加水平居中
sx={{
width: '100%', // 确保占满整个单元格宽度
minHeight: '40px' // 设置最小高度确保垂直居中
width: '100%', // 确保占满整个单元格宽度
minHeight: '40px' // 设置最小高度确保垂直居中
}}
>
{/* 恢复 TextField 用于正常数量输入 */}
{/* 恢复 TextField 用于正常数量输入 */}
<TextField
type="number"
size="small"
@@ -763,7 +763,7 @@ const LotTable: React.FC<LotTableProps> = ({
placeholder="0"
/>
{/* 添加 Pick Form 按钮用于问题情况 */}
{/* 添加 Pick Form 按钮用于问题情况 */}
<Button
variant="outlined"
size="small"
@@ -806,12 +806,12 @@ const LotTable: React.FC<LotTableProps> = ({
disabled={
(lot.lotAvailability === 'expired' ||
lot.lotAvailability === 'status_unavailable' ||
lot.lotAvailability === 'rejected') || // 添加 rejected
lot.lotAvailability === 'rejected') || // 添加 rejected
!pickQtyData[selectedRowId!]?.[lot.lotId] ||
!lot.stockOutLineStatus ||
!['pending','checked', 'partially_completed'].includes(lot.stockOutLineStatus.toLowerCase())
}
// Allow submission for available AND insufficient_stock lots
// Allow submission for available AND insufficient_stock lots
sx={{
fontSize: '0.75rem',
py: 0.5,
@@ -829,7 +829,7 @@ const LotTable: React.FC<LotTableProps> = ({
</Table>
</TableContainer>
{/* Status Messages Display */}
{/* Status Messages Display */}
{paginatedLotTableData.length > 0 && (
<Box sx={{ mt: 2, p: 2, backgroundColor: 'grey.50', borderRadius: 1 }}>
{paginatedLotTableData.map((lot, index) => (
@@ -858,7 +858,7 @@ const LotTable: React.FC<LotTableProps> = ({
}
/>
{/* QR Code Modal */}
{/* QR Code Modal */}
<QrCodeModal
open={qrModalOpen}
onClose={() => {
@@ -871,7 +871,7 @@ const LotTable: React.FC<LotTableProps> = ({
onQrCodeSubmit={handleQrCodeSubmit}
/>

{/* Pick Execution Form Modal */}
{/* Pick Execution Form Modal */}
{pickExecutionFormOpen && selectedLotForExecutionForm && selectedRow && (
<PickExecutionForm
open={pickExecutionFormOpen}


+ 20
- 20
src/components/PickOrderSearch/PickExecution.tsx View File

@@ -69,7 +69,7 @@ import SearchBox, { Criterion } from "../SearchBox";
import dayjs from "dayjs";
import { CreateStockOutLine } from "@/app/api/pickOrder/actions";
import LotTable from './LotTable';
import PickOrderDetailsTable from './PickOrderDetailsTable'; // Import the new component
import PickOrderDetailsTable from './PickOrderDetailsTable'; // Import the new component
import { updateInventoryLotLineStatus, updateInventoryStatus, updateInventoryLotLineQuantities } from "@/app/api/inventory/actions";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
@@ -147,7 +147,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
const [selectedLotRowId, setSelectedLotRowId] = useState<string | null>(null);
const [selectedLotId, setSelectedLotId] = useState<number | null>(null);

// Keep only the main table paging controller
// Keep only the main table paging controller
const [mainTablePagingController, setMainTablePagingController] = useState({
pageNum: 0,
pageSize: 10,
@@ -383,14 +383,14 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}
try {
// FIXED: 计算累计拣货数量
// FIXED: 计算累计拣货数量
const totalPickedForThisLot = (selectedLot.actualPickQty || 0) + qty;
console.log(" DEBUG - Previous picked:", selectedLot.actualPickQty || 0);
console.log("🔍 DEBUG - Current submit:", qty);
console.log("🔍 DEBUG - Total picked:", totalPickedForThisLot);
console.log("�� DEBUG - Required qty:", selectedLot.requiredQty);
// FIXED: 状态应该基于累计拣货数量
// FIXED: 状态应该基于累计拣货数量
let newStatus = 'partially_completed';
if (totalPickedForThisLot >= selectedLot.requiredQty) {
newStatus = 'completed';
@@ -405,7 +405,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
qty: qty
});
console.log(" Stock out line updated:", stockOutLineUpdate);
console.log(" Stock out line updated:", stockOutLineUpdate);
} catch (error) {
console.error("❌ Error updating stock out line:", error);
@@ -423,11 +423,11 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log("Inventory lot line updated:", inventoryLotLineUpdate);
}
// RE-ENABLE: Check if pick order should be completed
// RE-ENABLE: Check if pick order should be completed
if (newStatus === 'completed') {
console.log(" Stock out line completed, checking if entire pick order is complete...");
console.log(" Stock out line completed, checking if entire pick order is complete...");
// 添加调试日志来查看所有 pick orders 的 consoCode
// 添加调试日志来查看所有 pick orders 的 consoCode
console.log("📋 DEBUG - All pick orders and their consoCodes:");
if (pickOrderDetails) {
pickOrderDetails.pickOrders.forEach((pickOrder, index) => {
@@ -435,7 +435,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
});
}
// FIXED: 直接查找 consoCode,不依赖 selectedRow
// FIXED: 直接查找 consoCode,不依赖 selectedRow
if (pickOrderDetails) {
let currentConsoCode: string | null = null;
@@ -443,7 +443,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
for (const pickOrder of pickOrderDetails.pickOrders) {
const foundLine = pickOrder.pickOrderLines.find(line => line.id === selectedRowId);
if (foundLine) {
// 直接使用 pickOrder.code 作为 consoCode
// 直接使用 pickOrder.code 作为 consoCode
currentConsoCode = pickOrder.consoCode;
console.log(`�� DEBUG - Found consoCode for line ${selectedRowId}: ${currentConsoCode} (from pick order ${pickOrder.id})`);
break;
@@ -545,7 +545,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
setSelectedItemForQc(item);
}, []);

// Main table pagination handlers
// Main table pagination handlers
const handleMainTablePageChange = useCallback((event: unknown, newPage: number) => {
setMainTablePagingController(prev => ({
...prev,
@@ -781,7 +781,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}
const stockOutLineData: CreateStockOutLine = {
consoCode: correctConsoCode || pickOrderDetails?.consoCode || "", // 使用正确的 consoCode
consoCode: correctConsoCode || pickOrderDetails?.consoCode || "", // 使用正确的 consoCode
pickOrderLineId: selectedRowId,
inventoryLotLineId: inventoryLotLineId,
qty: 0.0
@@ -806,7 +806,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
await handleFetchAllPickOrderDetails();
console.log(" Data refresh completed - lot selection maintained!");
console.log(" Data refresh completed - lot selection maintained!");
} catch (refreshError) {
console.error("❌ Error refreshing data:", refreshError);
}
@@ -834,13 +834,13 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
setSelectedLotRowId(currentSelectedLotRowId);
setSelectedLotId(currentSelectedLotId);
console.log(" Data refreshed with selection preserved");
console.log(" Data refreshed with selection preserved");
} catch (error) {
console.error("❌ Error refreshing data:", error);
}
}, [selectedRowId, selectedLotRowId, selectedLotId, handleRowSelect, handleFetchAllPickOrderDetails]);

// Search criteria
// Search criteria
const searchCriteria: Criterion<any>[] = useMemo(
() => [
{
@@ -868,7 +868,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
[t],
);

// Search handler
// Search handler
const handleSearch = useCallback((query: Record<string, any>) => {
setSearchQuery({ ...query });
console.log("Search query:", query);
@@ -899,7 +899,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
console.log("Filtered pick orders count:", filtered.length);
}, [originalPickOrderData, t]);

// Reset handler
// Reset handler
const handleReset = useCallback(() => {
setSearchQuery({});
if (originalPickOrderData) {
@@ -907,7 +907,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
}
}, [originalPickOrderData]);

// Debug the lot data
// Debug the lot data
useEffect(() => {
console.log("Lot data:", lotData);
console.log("Pick Qty Data:", pickQtyData);
@@ -925,7 +925,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
/>
</Box>

{/* Main table using the new component */}
{/* Main table using the new component */}
<Box>
<Typography variant="h6" gutterBottom>
{t("Pick Order Details")}
@@ -969,7 +969,7 @@ const PickExecution: React.FC<Props> = ({ filterArgs }) => {
setShowInputBody={setShowInputBody}
selectedLotForInput={selectedLotForInput}
generateInputBody={generateInputBody}
// Add missing props
// Add missing props
totalPickedByAllPickOrders={0} // You can calculate this from lotData if needed
outQty={0} // You can calculate this from lotData if needed
holdQty={0} // You can calculate this from lotData if needed


+ 10
- 10
src/components/PickOrderSearch/PickExecutionForm.tsx View File

@@ -53,7 +53,7 @@ interface PickExecutionFormProps {
selectedPickOrderLine: (GetPickOrderLineInfo & { pickOrderCode: string }) | null;
pickOrderId?: number;
pickOrderCreateDate: any;
// Remove these props since we're not handling normal cases
// Remove these props since we're not handling normal cases
// onNormalPickSubmit?: (lineId: number, lotId: number, qty: number) => Promise<void>;
// selectedRowId?: number | null;
}
@@ -75,7 +75,7 @@ const PickExecutionForm: React.FC<PickExecutionFormProps> = ({
selectedPickOrderLine,
pickOrderId,
pickOrderCreateDate,
// Remove these props
// Remove these props
// onNormalPickSubmit,
// selectedRowId,
}) => {
@@ -166,7 +166,7 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
}
}, [errors]);

// Update form validation to require either missQty > 0 OR badItemQty > 0
// Update form validation to require either missQty > 0 OR badItemQty > 0
const validateForm = (): boolean => {
const newErrors: FormErrors = {};
@@ -174,17 +174,17 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
newErrors.actualPickQty = t('Qty is required');
}
// ADD: Check if actual pick qty exceeds remaining available qty
// ADD: Check if actual pick qty exceeds remaining available qty
if (formData.actualPickQty && formData.actualPickQty > remainingAvailableQty) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than remaining available qty');
}
// ADD: Check if actual pick qty exceeds required qty
// ADD: Check if actual pick qty exceeds required qty
if (formData.actualPickQty && formData.actualPickQty > requiredQty) {
newErrors.actualPickQty = t('Qty is not allowed to be greater than required qty');
}
// NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported)
// NEW: Require either missQty > 0 OR badItemQty > 0 (at least one issue must be reported)
const hasMissQty = formData.missQty && formData.missQty > 0;
const hasBadItemQty = formData.badItemQty && formData.badItemQty > 0;
@@ -229,11 +229,11 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
return (
<Dialog open={open} onClose={handleClose} maxWidth="sm" fullWidth>
<DialogTitle>
{t('Pick Execution Issue Form')} {/* Always show issue form title */}
{t('Pick Execution Issue Form')} {/* Always show issue form title */}
</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2 }}>
{/* Add instruction text */}
{/* Add instruction text */}
<Grid container spacing={2}>
<Grid item xs={12}>
<Box sx={{ p: 2, backgroundColor: '#fff3cd', borderRadius: 1, mb: 2 }}>
@@ -243,7 +243,7 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
</Box>
</Grid>
{/* Keep the existing form fields */}
{/* Keep the existing form fields */}
<Grid item xs={6}>
<TextField
fullWidth
@@ -305,7 +305,7 @@ const calculateRequiredQty = useCallback((lot: LotPickData) => {
/>
</Grid>
{/* Show issue description and handler fields when bad items > 0 */}
{/* Show issue description and handler fields when bad items > 0 */}
{(formData.badItemQty && formData.badItemQty > 0) ? (
<>
<Grid item xs={12}>


+ 3
- 3
src/components/PickOrderSearch/PickOrderDetailsTable.tsx View File

@@ -49,7 +49,7 @@ const PickOrderDetailsTable: React.FC<PickOrderDetailsTableProps> = ({
const availableQty = line.availableQty ?? 0;
const balanceToPick = availableQty - line.requiredQty;
// Handle both string and array date formats from the optimized API
// Handle both string and array date formats from the optimized API
let formattedTargetDate = 'N/A';
if (pickOrder.targetDate) {
if (typeof pickOrder.targetDate === 'string') {
@@ -66,14 +66,14 @@ const PickOrderDetailsTable: React.FC<PickOrderDetailsTableProps> = ({
pickOrderCode: pickOrder.code,
targetDate: formattedTargetDate,
balanceToPick: balanceToPick,
pickedQty: line.pickedQty, // This now comes from the optimized API
pickedQty: line.pickedQty, // This now comes from the optimized API
availableQty: availableQty,
};
})
);
}, [pickOrderDetails]);

// Paginated data
// Paginated data
const paginatedMainTableData = useMemo(() => {
const startIndex = pageNum * pageSize;
const endIndex = startIndex + pageSize;


+ 38
- 38
src/components/PickOrderSearch/PickQcStockInModalVer3.tsx View File

@@ -33,7 +33,7 @@ import EscalationComponent from "../PoDetail/EscalationComponent";
import { fetchPickOrderQcResult, savePickOrderQcResult } from "@/app/api/qc/actions";
import {
updateInventoryLotLineStatus
} from "@/app/api/inventory/actions"; // 导入新的 API
} from "@/app/api/inventory/actions"; // 导入新的 API
import { dayjsToDateTimeString } from "@/app/utils/formatUtil";
import dayjs from "dayjs";

@@ -42,8 +42,8 @@ interface ExtendedQcItem extends QcItemWithChecks {
qcPassed?: boolean;
failQty?: number;
remarks?: string;
order?: number; // Add order property
stableId?: string; // Also add stableId for better row identification
order?: number; // Add order property
stableId?: string; // Also add stableId for better row identification
}
interface Props extends CommonProps {
itemDetail: GetPickOrderLineInfo & {
@@ -55,7 +55,7 @@ interface Props extends CommonProps {
selectedLotId?: number;
onStockOutLineUpdate?: () => void;
lotData: LotPickData[];
// Add missing props
// Add missing props
pickQtyData?: PickQtyData;
selectedRowId?: number;
}
@@ -104,7 +104,7 @@ interface Props extends CommonProps {
};
qcItems: ExtendedQcItem[]; // Change to ExtendedQcItem
setQcItems: Dispatch<SetStateAction<ExtendedQcItem[]>>; // Change to ExtendedQcItem
// Add props for stock out line update
// Add props for stock out line update
selectedLotId?: number;
onStockOutLineUpdate?: () => void;
lotData: LotPickData[];
@@ -193,7 +193,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
failQty: item.isPassed ? 0 : (item.failQty || 0), // 0 for passed, actual qty for failed
type: "pick_order_qc",
remarks: item.remarks || "",
qcPassed: item.isPassed, // This will now be included
qcPassed: item.isPassed, // This will now be included
}));

// Store the submitted data for debug display
@@ -217,17 +217,17 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
}
};

// 修改:在组件开始时自动设置失败数量
// 修改:在组件开始时自动设置失败数量
useEffect(() => {
if (itemDetail && qcItems.length > 0 && selectedLotId) {
// 获取选中的批次数据
// 获取选中的批次数据
const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId);
if (selectedLot) {
// 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty
// 自动将 Lot Required Pick Qty 设置为所有失败项目的 failQty
const updatedQcItems = qcItems.map((item, index) => ({
...item,
failQty: selectedLot.requiredQty || 0, // 使用 Lot Required Pick Qty
// Add stable order and ID fields
// Add stable order and ID fields
order: index,
stableId: `qc-${item.id}-${index}`
}));
@@ -236,7 +236,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
}
}, [itemDetail, qcItems.length, selectedLotId, lotData]);

// Add this helper function at the top of the component
// Add this helper function at the top of the component
const safeClose = useCallback(() => {
if (onClose) {
// Create a mock event object that satisfies the Modal onClose signature
@@ -259,12 +259,12 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
isPersistent: () => false
} as any;
// Fixed: Pass both event and reason parameters
// Fixed: Pass both event and reason parameters
onClose(mockEvent, 'escapeKeyDown'); // 'escapeKeyDown' is a valid reason
}
}, [onClose]);

// 修改:移除 alert 弹窗,改为控制台日志
// 修改:移除 alert 弹窗,改为控制台日志
const onSubmitQc = useCallback<SubmitHandler<any>>(
async (data, event) => {
setIsSubmitting(true);
@@ -276,7 +276,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
const validationErrors : string[] = [];
const selectedLot = lotData.find(lot => lot.stockOutLineId === selectedLotId);
// Add safety check for selectedLot
// Add safety check for selectedLot
if (!selectedLot) {
console.error("Selected lot not found");
return;
@@ -313,23 +313,23 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
return;
}
// Handle different QC decisions
// Handle different QC decisions
if (selectedLotId) {
try {
const allPassed = qcData.qcItems.every(item => item.isPassed);
if (qcDecision === "2") {
// QC Decision 2: Report and Re-pick
// QC Decision 2: Report and Re-pick
console.log("QC Decision 2 - Report and Re-pick: Rejecting lot and marking as unavailable");
// Inventory lot line status: unavailable
// Inventory lot line status: unavailable
if (selectedLot) {
try {
console.log("=== DEBUG: Updating inventory lot line status ===");
console.log("Selected lot:", selectedLot);
console.log("Selected lot ID:", selectedLotId);
// FIX: Only send the fields that the backend expects
// FIX: Only send the fields that the backend expects
const updateData = {
inventoryLotLineId: selectedLot.lotId,
status: 'unavailable'
@@ -339,7 +339,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
console.log("Update data:", updateData);
const result = await updateInventoryLotLineStatus(updateData);
console.log(" Inventory lot line status updated successfully:", result);
console.log(" Inventory lot line status updated successfully:", result);
} catch (error) {
console.error("❌ Error updating inventory lot line status:", error);
@@ -359,28 +359,28 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
return;
}
// Close modal and refresh data
safeClose(); // Fixed: Use safe close function with both parameters
// Close modal and refresh data
safeClose(); // Fixed: Use safe close function with both parameters
if (onStockOutLineUpdate) {
onStockOutLineUpdate();
}
} else if (qcDecision === "1") {
// QC Decision 1: Accept
// QC Decision 1: Accept
console.log("QC Decision 1 - Accept: QC passed");
// Stock out line status: checked (QC completed)
// Stock out line status: checked (QC completed)
await updateStockOutLineStatus({
id: selectedLotId,
status: 'checked',
qty: acceptQty || 0
});
// Inventory lot line status: NO CHANGE needed
// Inventory lot line status: NO CHANGE needed
// Keep the existing status from handleSubmitPickQty
// Close modal and refresh data
safeClose(); // Fixed: Use safe close function with both parameters
// Close modal and refresh data
safeClose(); // Fixed: Use safe close function with both parameters
if (onStockOutLineUpdate) {
onStockOutLineUpdate();
}
@@ -399,7 +399,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
console.log("QC results saved successfully!");
// Show warning dialog for failed QC items when accepting
// Show warning dialog for failed QC items when accepting
if (qcDecision === "1" && !qcData.qcItems.every((q) => q.isPassed)) {
submitDialogWithWarning(() => {
closeHandler?.({}, 'escapeKeyDown');
@@ -448,7 +448,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
value={current.qcPassed === undefined ? "" : (current.qcPassed ? "true" : "false")}
onChange={(e) => {
const value = e.target.value === "true";
// Simple state update
// Simple state update
setQcItems(prev =>
prev.map(item =>
item.id === params.id
@@ -490,10 +490,10 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
<TextField
type="number"
size="small"
// 修改:失败项目自动显示 Lot Required Pick Qty
// 修改:失败项目自动显示 Lot Required Pick Qty
value={!params.row.qcPassed ? (0) : 0}
disabled={params.row.qcPassed}
// 移除 onChange,因为数量是固定的
// 移除 onChange,因为数量是固定的
// onChange={(e) => {
// const v = e.target.value;
// const next = v === "" ? undefined : Number(v);
@@ -535,7 +535,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
[t],
);

// Add stable update function
// Add stable update function
const handleQcResultChange = useCallback((itemId: number, qcPassed: boolean) => {
setQcItems(prevItems =>
prevItems.map(item =>
@@ -546,16 +546,16 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
);
}, []);

// Remove duplicate functions
// Remove duplicate functions
const getRowId = useCallback((row: any) => {
return row.id; // Just use the original ID
}, []);

// Remove complex sorting logic
// Remove complex sorting logic
// const stableQcItems = useMemo(() => { ... }); // Remove
// const sortedQcItems = useMemo(() => { ... }); // Remove

// Use qcItems directly in DataGrid
// Use qcItems directly in DataGrid
return (
<>
<FormProvider {...formProps}>
@@ -593,9 +593,9 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
<StyledDataGrid
columns={qcColumns}
rows={qcItems} // Use qcItems directly
rows={qcItems} // Use qcItems directly
autoHeight
getRowId={getRowId} // Simple row ID function
getRowId={getRowId} // Simple row ID function
/>
</Grid>
</>
@@ -636,7 +636,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
/>

{/* Combirne options 2 & 3 into one */}
{/* Combirne options 2 & 3 into one */}
<FormControlLabel
value="2"
control={<Radio />}
@@ -649,7 +649,7 @@ const PickQcStockInModalVer3: React.FC<Props> = ({
</FormControl>
</Grid>

{/* Show escalation component when QC Decision = 2 (Report and Re-pick) */}
{/* Show escalation component when QC Decision = 2 (Report and Re-pick) */}

<Grid item xs={12} sx={{ mt: 2 }}>


+ 2
- 2
src/components/PickOrderSearch/newcreatitem.tsx View File

@@ -568,7 +568,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
return;
}
// 修复:自动填充 type 为 "Consumable",不再强制用户选择
// 修复:自动填充 type 为 "Consumable",不再强制用户选择
// if (!data.type) {
// alert(t("Please select product type"));
// return;
@@ -625,7 +625,7 @@ const handleQtyBlur = useCallback((itemId: number) => {
}
}
// 修复:自动使用 "Consumable" 作为默认 type
// 修复:自动使用 "Consumable" 作为默认 type
const pickOrderData: SavePickOrderRequest = {
type: data.type || "Consumable", // 如果用户选择了 type 就用用户的,否则默认 "Consumable"
targetDate: formattedTargetDate,


+ 6
- 6
src/components/ProductionProcess/MachineScanner.tsx View File

@@ -72,18 +72,18 @@ const MachineScanner: React.FC<MachineScannerProps> = ({
try {
let machineCode: string;
// 尝试解析 JSON
// 尝试解析 JSON
try {
const scannedObj: MachineQrCode = JSON.parse(scannedInput);
machineCode = scannedObj.code;
} catch (jsonError) {
// 如果不是 JSON,尝试从花括号中提取
// 如果不是 JSON,尝试从花括号中提取
const match = scannedInput.match(/\{([^}?]+)\??}?/);
if (match && match[1]) {
machineCode = match[1].trim();
console.log("Extracted machine code from braces:", machineCode);
} else {
// 如果没有花括号,直接使用输入值
// 如果没有花括号,直接使用输入值
machineCode = scannedInput.replace(/[{}?]/g, '').trim();
console.log("Using plain machine code:", machineCode);
}
@@ -94,7 +94,7 @@ const MachineScanner: React.FC<MachineScannerProps> = ({
return;
}

// 首先尝试从 API 获取
// 首先尝试从 API 获取
const response = await isCorrectMachineUsed(machineCode);

if (response.message === "Success") {
@@ -109,7 +109,7 @@ const MachineScanner: React.FC<MachineScannerProps> = ({
target.value = "";
setScanError(null);
} else {
// 如果 API 失败,尝试从本地默认数据查找
// 如果 API 失败,尝试从本地默认数据查找
const localMachine = machineDatabase.find(
(m) => m.code.toLowerCase() === machineCode.toLowerCase()
);
@@ -125,7 +125,7 @@ const MachineScanner: React.FC<MachineScannerProps> = ({
target.value = "";
setScanError(null);
console.log(" Used local machine data:", localMachine);
console.log(" Used local machine data:", localMachine);
} else {
setScanError(
"Machine not found. Please check the code and try again."


+ 8
- 8
src/components/ProductionProcess/OperatorScanner.tsx View File

@@ -13,7 +13,7 @@ import {
import CloseIcon from "@mui/icons-material/Close";
import { isOperatorExist } from "@/app/api/jo/actions";
import { OperatorQrCode } from "./types";
// 新增:导入 user API
// 新增:导入 user API
import { fetchUserDetails } from "@/app/api/user/actions";
import { fetchNameList } from "@/app/api/user/actions";

@@ -71,19 +71,19 @@ const OperatorScanner: React.FC<OperatorScannerProps> = ({
console.log("Raw input:", usernameInput);
try {
// 检查是否是测试快捷格式 {2fitest<id>}
// 检查是否是测试快捷格式 {2fitest<id>}
const testMatch = usernameInput.match(/\{2fitest(\d+)\??}?/i);
if (testMatch && testMatch[1]) {
const userId = parseInt(testMatch[1]);
console.log(`🧪 Test mode: Fetching user with ID ${userId} from API`);
try {
// 方案 1:使用 fetchNameList 获取所有用户,然后找到对应 ID
// 方案 1:使用 fetchNameList 获取所有用户,然后找到对应 ID
const nameList = await fetchNameList();
const matchedUser = nameList.find(user => user.id === userId);
if (matchedUser) {
// 将 NameList 转换为 Operator 格式
// 将 NameList 转换为 Operator 格式
const operator: Operator = {
id: matchedUser.id,
name: matchedUser.name,
@@ -100,7 +100,7 @@ const OperatorScanner: React.FC<OperatorScannerProps> = ({
target.value = "";
setScanError(null);
console.log(` Added operator from API:`, operator);
console.log(` Added operator from API:`, operator);
return;
} else {
setScanError(
@@ -121,18 +121,18 @@ const OperatorScanner: React.FC<OperatorScannerProps> = ({
let username: string;
// 尝试解析 JSON
// 尝试解析 JSON
try {
const usernameObj: OperatorQrCode = JSON.parse(usernameInput);
username = usernameObj.username;
} catch (jsonError) {
// 如果不是 JSON,尝试从花括号中提取
// 如果不是 JSON,尝试从花括号中提取
const match = usernameInput.match(/\{([^}?]+)\??}?/);
if (match && match[1]) {
username = match[1].trim();
console.log("Extracted username from braces:", username);
} else {
// 如果没有花括号,直接使用输入值
// 如果没有花括号,直接使用输入值
username = usernameInput.replace(/[{}?]/g, '').trim();
console.log("Using plain username:", username);
}


+ 656
- 0
src/components/ProductionProcess/ProductionProcessDetail.tsx View File

@@ -0,0 +1,656 @@
"use client";
import React, { useCallback, useEffect, useState, useRef } from "react";
import {
Box,
Button,
Paper,
Stack,
Typography,
TextField,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Chip,
Card,
CardContent,
CircularProgress,
} from "@mui/material";
import QrCodeIcon from '@mui/icons-material/QrCode';
import { useTranslation } from "react-i18next";
import { Operator, Machine } from "@/app/api/jo";
import { useQrCodeScannerContext } from '../QrCodeScannerProvider/QrCodeScannerProvider';
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import {
fetchProductProcessById,
fetchProductProcessLines,
updateProductProcessLineQrscan,
fetchProductProcessLineDetail,
ProductProcessResponse,
ProductProcessLineResponse,
startProductProcessLine
} from "@/app/api/jo/actions";
import { fetchNameList, NameList } from "@/app/api/user/actions";

// 添加设备数据库(从 MachineScanner.tsx)
const machineDatabase: Machine[] = [
{ id: 1, name: "CNC Mill #1", code: "CNC001", qrCode: "QR-CNC001" },
{ id: 2, name: "Lathe #2", code: "LAT002", qrCode: "QR-LAT002" },
{ id: 3, name: "Press #3", code: "PRS003", qrCode: "QR-PRS003" },
{ id: 4, name: "Welder #4", code: "WLD004", qrCode: "QR-WLD004" },
{ id: 5, name: "Drill Press #5", code: "DRL005", qrCode: "QR-DRL005" },
];

interface ProcessLine {
id: number;
seqNo: number;
name: string;
description?: string;
equipmentType?: string;
startTime?: string;
endTime?: string;
outputFromProcessQty?: number;
outputFromProcessUom?: string;
defectQty?: number;
scrapQty?: number;
byproductName?: string;
byproductQty?: number;
handlerId?: number; // 添加 handlerId
}

interface ProductProcessDetailProps {
processId: number;
onBack: () => void;
}

const ProductionProcessDetail: React.FC<ProductProcessDetailProps> = ({
processId,
onBack,
}) => {
const { t } = useTranslation();
const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined;
const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext();
// 基本信息
const [processData, setProcessData] = useState<any>(null);
const [lines, setLines] = useState<ProcessLine[]>([]);
const [loading, setLoading] = useState(false);
// 选中的 line 和执行状态
const [selectedLineId, setSelectedLineId] = useState<number | null>(null);
const [isExecutingLine, setIsExecutingLine] = useState(false);
// 扫描器状态
const [isManualScanning, setIsManualScanning] = useState(false);
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [scannedOperators, setScannedOperators] = useState<Operator[]>([]);
const [scannedMachines, setScannedMachines] = useState<Machine[]>([]);
// 产出表单
const [outputData, setOutputData] = useState({
byproductName: "",
byproductQty: "",
byproductUom: "",
scrapQty: "",
scrapUom: "",
defectQty: "",
defectUom: "",
outputFromProcessQty: "",
outputFromProcessUom: "",
});

// 处理 QR 码扫描
// 处理 QR 码扫描
const processQrCode = useCallback((qrValue: string) => {
// 操作员格式:{2fitestu1} - 键盘模拟输入(测试用)
if (qrValue.match(/\{2fitestu(\d+)\}/)) {
const match = qrValue.match(/\{2fitestu(\d+)\}/);
const userId = parseInt(match![1]);
// 调用 API 获取用户信息
fetchNameList().then((users: NameList[]) => {
const user = users.find((u: NameList) => u.id === userId);
if (user) {
setScannedOperators([{
id: user.id,
name: user.name,
username: user.name
}]);
updateProductProcessLineQrscan({
lineId: selectedLineId || 0 as number,
operatorId: user.id,
});
}
});
return;
}
// 设备格式:{2fiteste1} - 键盘模拟输入(测试用)
if (qrValue.match(/\{2fiteste(\d+)\}/)) {
const match = qrValue.match(/\{2fiteste(\d+)\}/);
const equipmentId = parseInt(match![1]);
// 使用本地设备数据库
const machine = machineDatabase.find((m: Machine) => m.id === equipmentId);
if (machine) {
setScannedMachines([machine]);
}
updateProductProcessLineQrscan({
lineId: selectedLineId || 0 as number,
equipmentId: equipmentId,
}).then((res) => {
console.log(res);
});
return;
}
// 正常 QR 扫描器扫描:格式为 "operatorId: 1" 或 "equipmentId: 1"
const trimmedValue = qrValue.trim();
// 检查 operatorId 格式
const operatorMatch = trimmedValue.match(/^operatorId:\s*(\d+)$/i);
if (operatorMatch) {
const operatorId = parseInt(operatorMatch[1]);
fetchNameList().then((users: NameList[]) => {
const user = users.find((u: NameList) => u.id === operatorId);
if (user) {
setScannedOperators([{
id: user.id,
name: user.name,
username: user.name
}]);
updateProductProcessLineQrscan({
lineId: selectedLineId || 0 as number,
operatorId: user.id,
});
} else {
console.warn(`User with ID ${operatorId} not found`);
}
});
return;
}
// 检查 equipmentId 格式
const equipmentMatch = trimmedValue.match(/^equipmentId:\s*(\d+)$/i);
if (equipmentMatch) {
const equipmentId = parseInt(equipmentMatch[1]);
const machine = machineDatabase.find((m: Machine) => m.id === equipmentId);
if (machine) {
setScannedMachines([machine]);
}
updateProductProcessLineQrscan({
lineId: selectedLineId || 0 as number,
equipmentId: equipmentId,
}).then((res) => {
console.log(res);
});
return;
}
// 其他格式处理(JSON、普通文本等)
try {
const qrData = JSON.parse(qrValue);
// TODO: 处理 JSON 格式的 QR 码
} catch {
// 普通文本格式
// TODO: 处理普通文本格式
}
}, [selectedLineId]);

// 处理 QR 码扫描效果
useEffect(() => {
if (isManualScanning && qrValues.length > 0 && isExecutingLine) {
const latestQr = qrValues[qrValues.length - 1];
if (processedQrCodes.has(latestQr)) {
return;
}
setProcessedQrCodes(prev => new Set(prev).add(latestQr));
processQrCode(latestQr);
}
}, [qrValues, isManualScanning, isExecutingLine, processedQrCodes, processQrCode]);

// 开始扫描
const handleStartScan = useCallback(() => {
setIsManualScanning(true);
setProcessedQrCodes(new Set());
startScan();
}, [startScan]);

// 停止扫描
const handleStopScan = useCallback(() => {
setIsManualScanning(false);
stopScan();
resetScan();
}, [stopScan, resetScan]);

// 获取 process 和 lines 数据
const fetchProcessDetail = useCallback(async () => {
setLoading(true);
try {
console.log(`🔍 Loading process detail for ID: ${processId}`);
const data = await fetchProductProcessLineDetail(processId);
setProcessData(data);
const linesData = await fetchProductProcessLineDetail(processId);
setLines([linesData]);
console.log(" Process data loaded:", data);
console.log(" Lines loaded:", linesData);
} catch (error) {
console.error("❌ Error loading process detail:", error);
alert(`无法加载生产流程 ID ${processId}。该记录可能不存在。`);
onBack();
} finally {
setLoading(false);
}
}, [processId, onBack]);

useEffect(() => {
fetchProcessDetail();
}, [fetchProcessDetail]);

// 开始执行某个 line
const handleStartLine = async (lineId: number) => {
if (!currentUserId) {
alert("Please login first!");
return;
}
try {
// 使用 Server Action 而不是直接 fetch
await startProductProcessLine(lineId, currentUserId);
console.log(` Starting line ${lineId} with handlerId: ${currentUserId}`);
setSelectedLineId(lineId);
setIsExecutingLine(true);
setScannedOperators([]);
setScannedMachines([]);
setOutputData({
byproductName: "",
byproductQty: "",
byproductUom: "",
scrapQty: "",
scrapUom: "",
defectQty: "",
defectUom: "",
outputFromProcessQty: "",
outputFromProcessUom: "",
});
// 刷新数据
await fetchProcessDetail();
} catch (error) {
console.error("Error starting line:", error);
alert("Failed to start line. Please try again.");
}
};

// 提交产出数据
const handleSubmitOutput = async () => {
if (!selectedLineId) return;

if (scannedOperators.length === 0 || scannedMachines.length === 0) {
alert("Please scan operator and machine first!");
return;
}

try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8090'}/product-process/lines/${selectedLineId}/output`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
outputQty: parseFloat(outputData.outputFromProcessQty) || 0,
outputUom: outputData.outputFromProcessUom,
defectQty: parseFloat(outputData.defectQty) || 0,
defectUom: outputData.defectUom,
scrapQty: parseFloat(outputData.scrapQty) || 0,
scrapUom: outputData.scrapUom,
byproductName: outputData.byproductName,
byproductQty: parseFloat(outputData.byproductQty) || 0,
byproductUom: outputData.byproductUom,
}),
});

if (response.ok) {
console.log(" Output data submitted successfully");
setIsExecutingLine(false);
setSelectedLineId(null);
handleStopScan(); // 停止扫描
fetchProcessDetail(); // 刷新数据
}
} catch (error) {
console.error("Error submitting output:", error);
}
};

const selectedLine = lines.find(l => l.id === selectedLineId);

if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
<CircularProgress/>
</Box>
);
}

return (
<Box>
{/* 返回按钮 */}
<Box sx={{ mb: 2 }}>
<Button variant="outlined" onClick={onBack}>
{t("Back to List")}
</Button>
</Box>

{/* ========== 第一部分:基本信息 ========== */}
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom fontWeight="bold">
{t("Production Process Information")}
</Typography>
<Stack spacing={2} direction="row" useFlexGap flexWrap="wrap">
<Typography variant="subtitle1">
<strong>{t("Process Code")}:</strong> {processData?.productProcessCode}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Status")}:</strong>{" "}
<Chip
label={t(processData?.status || 'pending')}
color={processData?.status === 'completed' ? 'success' : 'primary'}
size="small"
/>
</Typography>
<Typography variant="subtitle1">
<strong>{t("Date")}:</strong> {dayjs(processData?.date).format(OUTPUT_DATE_FORMAT)}
</Typography>
<Typography variant="subtitle1">
<strong>{t("Total Steps")}:</strong> {lines.length}
</Typography>
</Stack>
</Paper>

{/* ========== 第二部分:Process Lines ========== */}
<Paper sx={{ p: 3 }}>
<Typography variant="h6" gutterBottom fontWeight="bold">
{t("Production Process Steps")}
</Typography>

{!isExecutingLine ? (
/* ========== 步骤列表视图 ========== */
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("Seq")}</TableCell>
<TableCell>{t("Step Name")}</TableCell>
<TableCell>{t("Description")}</TableCell>
<TableCell>{t("Equipment Type")}</TableCell>
<TableCell align="center">{t("Status")}</TableCell>
<TableCell align="center">{t("Action")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{lines.map((line) => (
<TableRow key={line.id}>
<TableCell>{line.seqNo}</TableCell>
<TableCell>
<Typography fontWeight={500}>{line.name}</Typography>
</TableCell>
<TableCell>{line.description}</TableCell>
<TableCell>{line.equipmentType}</TableCell>
<TableCell align="center">
{line.endTime ? (
<Chip label={t("Completed")} color="success" size="small" />
) : line.startTime ? (
<Chip label={t("In Progress")} color="primary" size="small" />
) : (
<Chip label={t("Pending")} color="default" size="small" />
)}
</TableCell>
<TableCell align="center">
<Button
variant="contained"
size="small"
startIcon={<PlayArrowIcon />}
onClick={() => handleStartLine(line.id)}
disabled={!!line.endTime}
>
{line.endTime ? t("Completed") : t("Start")}
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
/* ========== 步骤执行视图 ========== */
<Box>
{/* 当前步骤信息 */}
<Card sx={{ mb: 3, bgcolor: 'primary.50', border: '2px solid', borderColor: 'primary.main' }}>
<CardContent>
<Typography variant="h6" color="primary.main" gutterBottom>
{t("Executing")}: {selectedLine?.name} (Seq: {selectedLine?.seqNo})
</Typography>
<Typography variant="body2" color="text.secondary">
{selectedLine?.description}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Equipment")}: {selectedLine?.equipmentType}
</Typography>
</CardContent>
</Card>

<Stack spacing={3}>
{/* 合并的扫描器 */}
<Paper sx={{ p: 3, mb: 3 }}>
<Typography variant="h6" gutterBottom>
{t("Scan Operator & Equipment")}
</Typography>
<Stack spacing={2}>
{/* 操作员扫描 */}
<Box>
<Typography variant="body2" color="text.secondary">
{scannedOperators.length > 0
? `${t("Operator")}: ${scannedOperators[0].name || scannedOperators[0].username}`
: t("Please scan operator code")
}
</Typography>
</Box>
{/* 设备扫描 */}
<Box>
<Typography variant="body2" color="text.secondary">
{scannedMachines.length > 0
? `${t("Equipment")}: ${scannedMachines[0].name || scannedMachines[0].code}`
: t("Please scan equipment code")
}
</Typography>
</Box>
{/* 单个扫描按钮 */}
<Button
variant={isManualScanning ? "outlined" : "contained"}
startIcon={<QrCodeIcon />}
onClick={isManualScanning ? handleStopScan : handleStartScan}
color={isManualScanning ? "secondary" : "primary"}
>
{isManualScanning ? t("Stop QR Scan") : t("Start QR Scan")}
</Button>
</Stack>
</Paper>

{/* ========== 产出输入表单 ========== */}
{scannedOperators.length > 0 && scannedMachines.length > 0 && (
<Paper sx={{ p: 3, bgcolor: 'grey.50' }}>
<Typography variant="h6" gutterBottom fontWeight={600}>
{t("Production Output Data Entry")}
</Typography>

<Table size="small">
<TableHead>
<TableRow>
<TableCell width="30%">{t("Type")}</TableCell>
<TableCell width="35%">{t("Quantity")}</TableCell>
<TableCell width="35%">{t("Unit of Measure")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{/* 步骤收成 */}
<TableRow>
<TableCell>
<Typography fontWeight={500}>{t("Output from Process")}</Typography>
</TableCell>
<TableCell>
<TextField
type="number"
fullWidth
size="small"
value={outputData.outputFromProcessQty}
onChange={(e) => setOutputData(prev => ({ ...prev, outputFromProcessQty: e.target.value }))}
/>
</TableCell>
<TableCell>
<TextField
fullWidth
size="small"
value={outputData.outputFromProcessUom}
onChange={(e) => setOutputData(prev => ({ ...prev, outputFromProcessUom: e.target.value }))}
placeholder="KG, L, PCS..."
/>
</TableCell>
</TableRow>

{/* 副产品 */}
<TableRow>
<TableCell>
<Stack>
<Typography fontWeight={500}>{t("By-product")}</Typography>
<TextField
fullWidth
size="small"
value={outputData.byproductName}
onChange={(e) => setOutputData(prev => ({ ...prev, byproductName: e.target.value }))}
placeholder={t("By-product name")}
sx={{ mt: 1 }}
/>
</Stack>
</TableCell>
<TableCell>
<TextField
type="number"
fullWidth
size="small"
value={outputData.byproductQty}
onChange={(e) => setOutputData(prev => ({ ...prev, byproductQty: e.target.value }))}
/>
</TableCell>
<TableCell>
<TextField
fullWidth
size="small"
value={outputData.byproductUom}
onChange={(e) => setOutputData(prev => ({ ...prev, byproductUom: e.target.value }))}
placeholder="KG, L, PCS..."
/>
</TableCell>
</TableRow>

{/* 次品 */}
<TableRow sx={{ bgcolor: 'warning.50' }}>
<TableCell>
<Typography fontWeight={500} color="warning.dark">{t("Defect")}</Typography>
</TableCell>
<TableCell>
<TextField
type="number"
fullWidth
size="small"
value={outputData.defectQty}
onChange={(e) => setOutputData(prev => ({ ...prev, defectQty: e.target.value }))}
/>
</TableCell>
<TableCell>
<TextField
fullWidth
size="small"
value={outputData.defectUom}
onChange={(e) => setOutputData(prev => ({ ...prev, defectUom: e.target.value }))}
placeholder="KG, L, PCS..."
/>
</TableCell>
</TableRow>

{/* 废品 */}
<TableRow sx={{ bgcolor: 'error.50' }}>
<TableCell>
<Typography fontWeight={500} color="error.dark">{t("Scrap")}</Typography>
</TableCell>
<TableCell>
<TextField
type="number"
fullWidth
size="small"
value={outputData.scrapQty}
onChange={(e) => setOutputData(prev => ({ ...prev, scrapQty: e.target.value }))}
/>
</TableCell>
<TableCell>
<TextField
fullWidth
size="small"
value={outputData.scrapUom}
onChange={(e) => setOutputData(prev => ({ ...prev, scrapUom: e.target.value }))}
placeholder="KG, L, PCS..."
/>
</TableCell>
</TableRow>
</TableBody>
</Table>

{/* 提交按钮 */}
<Box sx={{ mt: 3, display: 'flex', gap: 2 }}>
<Button
variant="outlined"
onClick={() => {
setIsExecutingLine(false);
setSelectedLineId(null);
handleStopScan();
}}
>
{t("Cancel")}
</Button>
<Button
variant="contained"
startIcon={<CheckCircleIcon />}
onClick={handleSubmitOutput}
>
{t("Complete Step")}
</Button>
</Box>
</Paper>
)}
</Stack>
</Box>
)}
</Paper>
</Box>
);
};

export default ProductionProcessDetail;

+ 185
- 0
src/components/ProductionProcess/ProductionProcessList.tsx View File

@@ -0,0 +1,185 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
import {
Box,
Button,
Card,
CardContent,
CardActions,
Stack,
Typography,
Chip,
CircularProgress,
TablePagination,
Grid,
} from "@mui/material";
import { useTranslation } from "react-i18next";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import dayjs from "dayjs";
import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import {
fetchAllJoborderProductProcessInfo,
AllJoborderProductProcessInfoResponse,
} from "@/app/api/jo/actions";

interface ProductProcessListProps {
onSelectProcess: (processId: number) => void;
}

const PER_PAGE = 6;

const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess }) => {
const { t } = useTranslation();
const { data: session } = useSession() as { data: SessionWithTokens | null };

const [loading, setLoading] = useState(false);
const [processes, setProcesses] = useState<AllJoborderProductProcessInfoResponse[]>([]);
const [page, setPage] = useState(0);

const fetchProcesses = useCallback(async () => {
setLoading(true);
try {
const data = await fetchAllJoborderProductProcessInfo();
setProcesses(data || []);
setPage(0);
} catch (e) {
console.error(e);
setProcesses([]);
} finally {
setLoading(false);
}
}, []);

useEffect(() => {
fetchProcesses();
}, [fetchProcesses]);

const startIdx = page * PER_PAGE;
const paged = processes.slice(startIdx, startIdx + PER_PAGE);

return (
<Box>
{loading ? (
<Box sx={{ display: "flex", justifyContent: "center", p: 3 }}>
<CircularProgress />
</Box>
) : (
<Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{t("Total processes")}: {processes.length}
</Typography>

<Grid container spacing={2}>
{paged.map((process) => {
const status = String(process.status || "");
const statusLower = status.toLowerCase();
const statusColor =
statusLower === "completed"
? "success"
: statusLower === "in_progress" || statusLower === "processing"
? "primary"
: "default";

const finishedCount =
(process as any).finishedProductProcessLineCount ??
(process as any).FinishedProductProcessLineCount ??
0;

const totalCount = process.productProcessLineCount ?? process.lines?.length ?? 0;
const linesWithStatus = (process.lines || []).filter(
(l) => String(l.status ?? "").trim() !== ""
);

const dateDisplay = process.date
? dayjs(process.date as any).format(OUTPUT_DATE_FORMAT)
: "-";
const jobOrderCode =
(process as any).jobOrderCode ??
(process.jobOrderId ? `JO-${process.jobOrderId}` : "N/A");
const inProgressLines = (process.lines || [])
.filter(l => String(l.status ?? "").trim() !== "")
.filter(l => String(l.status).toLowerCase() === "in_progress");

return (
<Grid key={process.id} item xs={12} sm={6} md={4}>
<Card
sx={{
minHeight: 160,
maxHeight: 240,
display: "flex",
flexDirection: "column",
}}
>
<CardContent
sx={{
pb: 1,
flexGrow: 1, // let content take remaining height
overflow: "auto", // allow scroll when content exceeds
}}
>
<Stack direction="row" justifyContent="space-between" alignItems="center">
<Box sx={{ minWidth: 0 }}>
<Typography variant="subtitle1">
{process.productProcessCode}
</Typography>
<Typography variant="body2" color="text.secondary">
{t("Job Order")}: {jobOrderCode}
</Typography>

</Box>
<Chip size="small" label={t(status)} color={statusColor as any} />
</Stack>

{statusLower !== "pending" && linesWithStatus.length > 0 && (
<Box sx={{ mt: 1 }}>
<Typography variant="body2" fontWeight={600}>
{t("Finished lines")}: {finishedCount} / {totalCount}
</Typography>

{inProgressLines.length > 0 && (
<Box sx={{ mt: 1 }}>
{inProgressLines.map(line => (
<Typography key={line.id} variant="caption" color="text.secondary" display="block">
{t("Operator")}: {line.operatorName || "-"} <br />
{t("Equipment")}: {line.equipmentName || "-"}
</Typography>
))}
</Box>
)}
</Box>
)}
</CardContent>

<CardActions sx={{ pt: 0.5 }}>
<Button variant="contained" size="small" onClick={() => onSelectProcess(process.id)}>
{t("View Details")}
</Button>
<Box sx={{ flex: 1 }} />
<Typography variant="caption" color="text.secondary">
{t("Lines")}: {totalCount}
</Typography>
</CardActions>
</Card>
</Grid>
);
})}
</Grid>

{processes.length > 0 && (
<TablePagination
component="div"
count={processes.length}
page={page}
rowsPerPage={PER_PAGE}
onPageChange={(e, p) => setPage(p)}
rowsPerPageOptions={[PER_PAGE]}
/>
)}
</Box>
)}
</Box>
);
};

export default ProductProcessList;

+ 67
- 0
src/components/ProductionProcess/ProductionProcessPage.tsx View File

@@ -0,0 +1,67 @@
"use client";
import React, { useState, useEffect, useCallback } from "react";
import { useSession } from "next-auth/react";
import { SessionWithTokens } from "@/config/authConfig";
import ProductionProcessList from "@/components/ProductionProcess/ProductionProcessList";
import ProductionProcessDetail from "@/components/ProductionProcess/ProductionProcessDetail";
import {
fetchProductProcesses,
fetchProductProcessLines,
ProductProcessLineResponse
} from "@/app/api/jo/actions";

const ProductionProcessPage = () => {
const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);
const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined;

const checkAndRedirectToDetail = useCallback(async () => {
if (!currentUserId) return;
try {
// 获取所有 processes
const processes = await fetchProductProcesses();
// 获取所有 lines 并检查是否有匹配的
for (const process of processes.content || []) {
const lines = await fetchProductProcessLines(process.id);
const pendingLine = lines.find((line: ProductProcessLineResponse) =>
line.handlerId === currentUserId &&
!line.endTime &&
line.startTime
);
if (pendingLine) {
setSelectedProcessId(process.id);
break;
}
}
} catch (error) {
console.error("Error checking pending lines:", error);
}
}, [currentUserId]);

useEffect(() => {
if (currentUserId && !selectedProcessId) {
// 检查是否有当前用户的 pending line
checkAndRedirectToDetail();
}
}, [currentUserId, selectedProcessId, checkAndRedirectToDetail]);

if (selectedProcessId !== null) {
return (
<ProductionProcessDetail
processId={selectedProcessId}
onBack={() => setSelectedProcessId(null)}
/>
);
}

return (
<ProductionProcessList
onSelectProcess={(id) => setSelectedProcessId(id)}
/>
);
};

export default ProductionProcessPage;

Loading…
Cancel
Save