Browse Source

update job, releasetype ,fast submit.

master
CANCERYS\kw093 4 days ago
parent
commit
a14b7f5869
14 changed files with 682 additions and 475 deletions
  1. +11
    -1
      src/app/api/do/actions.tsx
  2. +89
    -3
      src/app/api/jo/actions.ts
  3. +2
    -0
      src/app/api/jo/index.ts
  4. +1
    -21
      src/app/api/pickOrder/actions.ts
  5. +41
    -5
      src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx
  6. +14
    -18
      src/components/JoSearch/JoSearch.tsx
  7. +232
    -186
      src/components/Jodetail/JobPickExecution.tsx
  8. +138
    -211
      src/components/Jodetail/newJobPickExecution.tsx
  9. +4
    -1
      src/components/ProductionProcess/ProductionProcessDetail.tsx
  10. +4
    -4
      src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx
  11. +49
    -4
      src/components/ProductionProcess/ProductionProcessList.tsx
  12. +81
    -20
      src/components/ProductionProcess/ProductionProcessStepExecution.tsx
  13. +9
    -1
      src/i18n/zh/common.json
  14. +7
    -0
      src/i18n/zh/jo.json

+ 11
- 1
src/app/api/do/actions.tsx View File

@@ -181,7 +181,17 @@ export const fetchTicketReleaseTable = cache(async ()=> {
}
);
});

export const startBatchReleaseAsyncSingle = cache(async (data: { doId: number; userId: number }) => {
const { doId, userId } = data;
return await serverFetchJson<{ id: number|null; code: string; entity?: any }>(
`${BASE_API_URL}/doPickOrder/batch-release/async-single?userId=${userId}`,
{
method: "POST",
body: JSON.stringify(doId),
headers: { "Content-Type": "application/json" },
}
);
});
export const startBatchReleaseAsync = cache(async (data: { ids: number[]; userId: number }) => {
const { ids, userId } = data;
return await serverFetchJson<{ id: number|null; code: string; entity?: any }>(


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

@@ -383,6 +383,8 @@ export interface JobOrderProcessLineDetailResponse {
byproductName: string;
byproductQty: number;
byproductUom: string;
productProcessIssueId: number;
productProcessIssueStatus: string;
}
export interface JobOrderLineInfo {
id: number,
@@ -453,6 +455,90 @@ export interface JobTypeResponse {
id: number;
name: string;
}
export interface SaveProductProcessIssueTimeRequest {
productProcessLineId: number;
reason: string;
}
export interface JobOrderLotsHierarchicalResponse {
pickOrder: PickOrderInfoResponse;
pickOrderLines: PickOrderLineWithLotsResponse[];
}

export interface PickOrderInfoResponse {
id: number | null;
code: string | null;
consoCode: string | null;
targetDate: string | null;
type: string | null;
status: string | null;
assignTo: number | null;
jobOrder: JobOrderBasicInfoResponse;
}

export interface JobOrderBasicInfoResponse {
id: number;
code: string;
name: string;
}

export interface PickOrderLineWithLotsResponse {
id: number;
itemId: number | null;
itemCode: string | null;
itemName: string | null;
requiredQty: number | null;
uomCode: string | null;
uomDesc: string | null;
status: string | null;
lots: LotDetailResponse[];
}

export interface LotDetailResponse {
lotId: number | null;
lotNo: string | null;
expiryDate: string | null;
location: string | null;
availableQty: number | null;
requiredQty: number | null;
actualPickQty: number | null;
processingStatus: string | null;
lotAvailability: string | null;
pickOrderId: number | null;
pickOrderCode: string | null;
pickOrderConsoCode: string | null;
pickOrderLineId: number | null;
stockOutLineId: number | null;
suggestedPickLotId: number | null;
stockOutLineQty: number | null;
stockOutLineStatus: string | null;
routerIndex: number | null;
routerArea: string | null;
routerRoute: string | null;
uomShortDesc: string | null;
matchStatus?: string | null;
matchBy?: number | null;
matchQty?: number | null;
}


export const saveProductProcessIssueTime = cache(async (request: SaveProductProcessIssueTimeRequest) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/ProcessLine/issue`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
}
);
});
export const saveProductProcessResumeTime = cache(async (productProcessIssueId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/Demo/ProcessLine/resume/${productProcessIssueId}`,
{
method: "POST",
}
);
});
export const deleteJobOrder=cache(async (jobOrderId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/jo/demo/deleteJobOrder/${jobOrderId}`,
@@ -480,7 +566,7 @@ export const updateJoPickOrderHandledBy = cache(async (request: UpdateJoPickOrde
);
});
export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrderId: number) => {
return serverFetchJson<any>(
return serverFetchJson<JobOrderLotsHierarchicalResponse>(
`${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order/${pickOrderId}`,
{
method: "GET",
@@ -568,7 +654,7 @@ export const startProductProcessLine = async (lineId: number) => {
};
export const completeProductProcessLine = async (lineId: number) => {
return serverFetchJson<any>(
`${BASE_API_URL}/product-process/demo/ProcessLine/complete/${lineId}`,
`${BASE_API_URL}/product-process/Demo/ProcessLine/complete/${lineId}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -715,7 +801,7 @@ export const assignJobOrderPickOrder = async (pickOrderId: number, userId: numbe

// 获取 Job Order 分层数据
export const fetchJobOrderLotsHierarchical = cache(async (userId: number) => {
return serverFetchJson<any>(
return serverFetchJson<JobOrderLotsHierarchicalResponse>(
`${BASE_API_URL}/jo/all-lots-hierarchical/${userId}`,
{
method: "GET",


+ 2
- 0
src/app/api/jo/index.ts View File

@@ -30,6 +30,8 @@ export interface JobOrder {
type: string;
jobTypeId: number;
jobTypeName: string;
sufficientCount: number;
insufficientCount: number;
// TODO pack below into StockInLineInfo
stockInLineId?: number;
stockInLineStatus?: string;


+ 1
- 21
src/app/api/pickOrder/actions.ts View File

@@ -1063,27 +1063,7 @@ export const fetchLotDetailsByDoPickOrderRecordId = async (doPickOrderRecordId:
};
}
};
// Update the existing function to use the non-auto-assign endpoint
export const fetchALLPickOrderLineLotDetails = cache(async (userId: number): Promise<any[]> => {
try {
console.log("🔍 Fetching all pick order line lot details for userId:", userId);
// Use the non-auto-assign endpoint
const data = await serverFetchJson<any[]>(
`${BASE_API_URL}/pickOrder/all-lots-with-details-no-auto-assign/${userId}`,
{
method: 'GET',
next: { tags: ["pickorder"] },
}
);
console.log(" Fetched lot details:", data);
return data;
} catch (error) {
console.error("❌ Error fetching lot details:", error);
return [];
}
});

export const fetchAllPickOrderDetails = cache(async (userId?: number) => {
if (!userId) {
return {


+ 41
- 5
src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx View File

@@ -25,7 +25,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw
const [isAssigning, setIsAssigning] = useState(false);
//const [selectedDate, setSelectedDate] = useState<string>("today");
const [selectedDate, setSelectedDate] = useState<string>("today");
const [releaseType, setReleaseType] = useState<string>("batch");
const loadSummaries = useCallback(async () => {
setIsLoadingSummary(true);
try {
@@ -40,8 +40,8 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw
}
const [s2, s4] = await Promise.all([
fetchStoreLaneSummary("2/F", dateParam),
fetchStoreLaneSummary("4/F", dateParam)
fetchStoreLaneSummary("2/F", dateParam, releaseType),
fetchStoreLaneSummary("4/F", dateParam, releaseType)
]);
setSummary2F(s2);
setSummary4F(s4);
@@ -50,7 +50,7 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw
} finally {
setIsLoadingSummary(false);
}
}, [selectedDate]);
}, [selectedDate, releaseType]);

// 初始化
useEffect(() => {
@@ -168,10 +168,34 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw
{t("Day After Tomorrow")} ({getDateLabel(2)})
</MenuItem>
</Select>
</FormControl>
</Box>
<Box sx={{minWidth: 140, maxWidth: 300 }}>
<FormControl fullWidth size="small">
<InputLabel id="release-type-select-label">{t("Release Type")}</InputLabel>
<Select
labelId="release-type-select-label"
id="release-type-select"
value={releaseType}
label={t("Release Type")}
onChange={(e) => { {
setReleaseType(e.target.value);
loadSummaries();
}}}
>

<MenuItem value="batch">
{t("Batch")}
</MenuItem>
<MenuItem value="single">
{t("Single")}
</MenuItem>
</Select>
</FormControl>
</Box>
<Box
sx={{
p: 1,
@@ -186,6 +210,13 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw
{t("EDT - Lane Code (Unassigned/Total)")}
</Typography>
</Box>
</Stack>
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: 'flex-start' }}>

</Stack>


@@ -391,6 +422,11 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw
</Stack>
</Grid>
</Grid>




</Box>
);
};


+ 14
- 18
src/components/JoSearch/JoSearch.tsx View File

@@ -120,20 +120,10 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
};

const getStockCounts = (jo: JobOrder) => {
const detailedJo = detailedJos.get(jo.id);
if (!detailedJo?.pickLines || detailedJo.pickLines.length === 0) {
return { total: 0, sufficient: 0, insufficient: 0 };
}

const totalLines = detailedJo.pickLines.length;
const sufficientLines = detailedJo.pickLines.filter(pickLine => isStockSufficient(pickLine)).length;
const insufficientLines = totalLines - sufficientLines;

return {
total: totalLines,
sufficient: sufficientLines,
insufficient: insufficientLines
sufficient: jo.sufficientCount,
insufficient: jo.insufficientCount
};
};

@@ -210,7 +200,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
const stockCounts = getStockCounts(row);
return (
<span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}>
{stockCounts.sufficient}/{stockCounts.total}
{stockCounts.sufficient}/{stockCounts.sufficient + stockCounts.insufficient}
</span>
);
}
@@ -229,17 +219,20 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
// onClick: (record) => onDetailClick(record),
// buttonIcon: <EditNote />,
renderCell: (row) => {
const btnSx = getButtonSx(row);
//const btnSx = getButtonSx(row);
return (
<Button
id="emailSupplier"
type="button"
variant="contained"
color="primary"
sx={{ width: "150px", backgroundColor: btnSx.color }}
// sx={{ width: "150px", backgroundColor: btnSx.color }}
sx={{ width: "150px" }}
// disabled={params.row.status != "rejected" && params.row.status != "partially_completed"}
onClick={() => onDetailClick(row)}
>{btnSx.label}</Button>
// >{btnSx.label}
>{t("View")}
</Button>
)
}
},
@@ -349,7 +342,7 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT

const [openModal, setOpenModal] = useState<boolean>(false);
const [modalInfo, setModalInfo] = useState<StockInLineInput>();
/*
const onDetailClick = useCallback((record: JobOrder) => {

if (record.status == "processing") {
@@ -367,7 +360,10 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
router.push(`/jo/edit?id=${record.id}`)
}
}, [])

*/
const onDetailClick = useCallback((record: JobOrder) => {
router.push(`/jo/edit?id=${record.id}`)
}, [])
const closeNewModal = useCallback(() => {
// const response = updateJo({ id: 1, status: "storing" });
setOpenModal(false); // Close the modal first


+ 232
- 186
src/components/Jodetail/JobPickExecution.tsx View File

@@ -34,13 +34,19 @@ import {
checkPickOrderCompletion,
PickOrderCompletionResponse,
checkAndCompletePickOrderByConsoCode,
confirmLotSubstitution
confirmLotSubstitution,
updateStockOutLineStatusByQRCodeAndLotNo,
batchSubmitList,
batchSubmitListRequest,
batchSubmitListLineRequest,
} from "@/app/api/pickOrder/actions";
// 修改:使用 Job Order API
import {
fetchJobOrderLotsHierarchical,
fetchUnassignedJobOrderPickOrders,
assignJobOrderPickOrder
assignJobOrderPickOrder,
updateJo,
JobOrderLotsHierarchicalResponse,
} from "@/app/api/jo/actions";
import { fetchNameList, NameList } from "@/app/api/user/actions";
import {
@@ -322,14 +328,13 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const { data: session } = useSession() as { data: SessionWithTokens | null };
const currentUserId = session?.id ? parseInt(session.id) : undefined;
const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null);
// 修改:使用 Job Order 数据结构
const [jobOrderData, setJobOrderData] = useState<any>(null);
const [combinedLotData, setCombinedLotData] = useState<any[]>([]);
const [combinedDataLoading, setCombinedDataLoading] = useState(false);
const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
const [filteredLotData, setFilteredLotData] = useState<any[]>([]);
// 添加未分配订单状态

const [combinedDataLoading, setCombinedDataLoading] = useState(false);

const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
@@ -375,6 +380,60 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);



const getAllLotsFromHierarchical = useCallback((
data: JobOrderLotsHierarchicalResponse | null
): any[] => {
if (!data || !data.pickOrder || !data.pickOrderLines) {
return [];
}

const allLots: any[] = [];
data.pickOrderLines.forEach((line) => {
if (line.lots && line.lots.length > 0) {
line.lots.forEach((lot) => {
allLots.push({
...lot,
pickOrderLineId: line.id,
itemId: line.itemId,
itemCode: line.itemCode,
itemName: line.itemName,
uomCode: line.uomCode,
uomDesc: line.uomDesc,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
jobOrderId: data.pickOrder.jobOrder.id,
jobOrderCode: data.pickOrder.jobOrder.code,
// 添加 pickOrder 信息(如果需要)
pickOrderId: data.pickOrder.id,
pickOrderCode: data.pickOrder.code,
pickOrderConsoCode: data.pickOrder.consoCode,
pickOrderTargetDate: data.pickOrder.targetDate,
pickOrderType: data.pickOrder.type,
pickOrderStatus: data.pickOrder.status,
pickOrderAssignTo: data.pickOrder.assignTo,
});
});
}
});
return allLots;
}, []);
const allLotsFromData = useMemo(() => {
return getAllLotsFromHierarchical(jobOrderData);
}, [jobOrderData, getAllLotsFromHierarchical]);
// 用于显示的 combinedLotData(支持搜索过滤)
const combinedLotData = useMemo(() => {
return filteredLotData.length > 0 ? filteredLotData : allLotsFromData;
}, [filteredLotData, allLotsFromData]);
// 用于搜索的原始数据
const originalCombinedData = useMemo(() => {
return allLotsFromData;
}, [allLotsFromData]);
// 修改:加载未分配的 Job Order 订单
const loadUnassignedOrders = useCallback(async () => {
setIsLoadingUnassigned(true);
@@ -466,113 +525,53 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
try {
const userIdToUse = userId || currentUserId;
console.log(" fetchJobOrderData called with userId:", userIdToUse);
if (!userIdToUse) {
console.warn("⚠️ No userId available, skipping API call");
setJobOrderData(null);
setCombinedLotData([]);
setOriginalCombinedData([]);
return;
}
window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
detail: {
hasData: false,
tabIndex: 0
}
}));
// 使用 Job Order API
// 直接使用类型化的响应
const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse);
console.log(" Job Order data:", jobOrderData);
console.log(" Job Order data (hierarchical):", jobOrderData);
setJobOrderData(jobOrderData);
// Transform hierarchical data to flat structure for the table
const flatLotData: any[] = [];
if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) {
jobOrderData.pickOrderLines.forEach((line: any) => {
if (line.lots && line.lots.length > 0) {
line.lots.forEach((lot: any) => {
flatLotData.push({
// Pick order info
pickOrderId: jobOrderData.pickOrder.id,
pickOrderCode: jobOrderData.pickOrder.code,
pickOrderConsoCode: jobOrderData.pickOrder.consoCode,
pickOrderTargetDate: jobOrderData.pickOrder.targetDate,
pickOrderType: jobOrderData.pickOrder.type,
pickOrderStatus: jobOrderData.pickOrder.status,
pickOrderAssignTo: jobOrderData.pickOrder.assignTo,
// Pick order line info
pickOrderLineId: line.id,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
// Item info
itemId: line.itemId,
itemCode: line.itemCode,
itemName: line.itemName,
uomCode: line.uomCode,
uomDesc: line.uomDesc,
// Lot info
lotId: lot.lotId,
lotNo: lot.lotNo,
expiryDate: lot.expiryDate,
location: lot.location,
availableQty: lot.availableQty,
requiredQty: lot.requiredQty,
actualPickQty: lot.actualPickQty,
lotStatus: lot.lotStatus,
lotAvailability: lot.lotAvailability,
processingStatus: lot.processingStatus,
stockOutLineId: lot.stockOutLineId,
stockOutLineStatus: lot.stockOutLineStatus,
stockOutLineQty: lot.stockOutLineQty,
suggestedPickLotId: lot.suggestedPickLotId,
// Router info
routerIndex: lot.routerIndex,
secondQrScanStatus: lot.secondQrScanStatus,
routerArea: lot.routerArea,
routerRoute: lot.routerRoute,
uomShortDesc: lot.uomShortDesc
});
});
}
});
}
// 使用辅助函数获取所有 lots(用于计算完成状态等)
const allLots = getAllLotsFromHierarchical(jobOrderData);
setFilteredLotData(allLots);
const hasData = allLots.length > 0;
console.log(" Transformed flat lot data:", flatLotData);
setCombinedLotData(flatLotData);
setOriginalCombinedData(flatLotData);
const hasData = flatLotData.length > 0;
window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
detail: {
hasData: hasData,
tabIndex: 0
}
}));
// 计算完成状态并发送事件
const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
// 计算完成状态
const allCompleted = allLots.length > 0 && allLots.every((lot) =>
lot.processingStatus === 'completed'
);
// 发送完成状态事件,包含标签页信息
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: allCompleted,
tabIndex: 0 // 明确指定这是来自标签页 0 的事件
tabIndex: 0
}
}));
} catch (error) {
console.error("❌ Error fetching job order data:", error);
setJobOrderData(null);
setCombinedLotData([]);
setOriginalCombinedData([]);
// 如果加载失败,禁用打印按钮
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
@@ -582,7 +581,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} finally {
setCombinedDataLoading(false);
}
}, [currentUserId]);
}, [currentUserId, getAllLotsFromHierarchical]);

// 修改:初始化时加载数据
useEffect(() => {
@@ -828,28 +827,70 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
setIsConfirmingLot(false);
}
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]);

const handleFastQrScan = useCallback(async (lotNo: string) => {
const matchingLot = combinedLotData.find(lot =>
lot.lotNo && lot.lotNo === lotNo
);
if (!matchingLot || !matchingLot.stockOutLineId) {
console.warn(`⚠️ Fast scan: Lot ${lotNo} not found or no stockOutLineId`);
return;
}
try {
const res = await updateStockOutLineStatusByQRCodeAndLotNo({
pickOrderLineId: matchingLot.pickOrderLineId,
inventoryLotNo: lotNo,
stockOutLineId: matchingLot.stockOutLineId,
itemId: matchingLot.itemId,
status: "checked",
});
if (res.code === "checked" || res.code === "SUCCESS") {
const entity = res.entity as any;
// ✅ 更新 filteredLotData(如果存在)或刷新数据
if (filteredLotData.length > 0) {
setFilteredLotData(prev => prev.map((lot: any) => {
if (lot.stockOutLineId === matchingLot.stockOutLineId &&
lot.pickOrderLineId === matchingLot.pickOrderLineId) {
return {
...lot,
stockOutLineStatus: 'checked',
stockOutLineQty: entity?.qty ? Number(entity.qty) : lot.stockOutLineQty,
};
}
return lot;
}));
}
// ✅ 刷新 jobOrderData 以更新所有计算值
await fetchJobOrderData();
console.log("✅ Fast scan completed successfully");
}
} catch (error) {
console.error(`❌ Fast scan error for ${lotNo}:`, error);
}
}, [combinedLotData, filteredLotData, 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;
}

// 1) Parse JSON safely
let qrData: any = null;
try {
qrData = JSON.parse(latestQr);
} catch {
console.log("QR is not JSON format");
// Handle non-JSON QR codes as direct lot numbers
const directLotNo = latestQr.replace(/[{}]/g, '');
if (directLotNo) {
console.log(`Processing direct lot number: ${directLotNo}`);
await handleQrCodeSubmit(directLotNo);
}
console.log("QR content is not JSON; skipping lotNo direct submit to avoid false matches.");
setQrScanError(true);
setQrScanSuccess(false);
return;
}

try {
// Only use the new API when we have JSON with stockInLineId + itemId
if (!(qrData?.stockInLineId && qrData?.itemId)) {
@@ -859,18 +900,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
return;
}
// First, fetch stock in line info to get the lot number
let stockInLineInfo: any;
try {
stockInLineInfo = await fetchStockInLineInfo(qrData.stockInLineId);
console.log("Stock in line info:", stockInLineInfo);
} catch (error) {
console.error("Error fetching stock in line info:", error);
setQrScanError(true);
setQrScanSuccess(false);
return;
}

// Call new analyze-qr-code API
const analysis = await analyzeQrCode({
itemId: qrData.itemId,
@@ -892,13 +921,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} = analysis || {};
// 1) Find all lots for the same item from current expected list
const sameItemLotsInExpected = combinedLotData.filter(l =>
const sameItemLotsInExpected = combinedLotData.filter((l: any) =>
(l.itemId && analyzedItemId && l.itemId === analyzedItemId) ||
(l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode)
);
if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) {
// Case 3: No item code match
console.error("No item match in expected lots for scanned code");
setQrScanError(true);
setQrScanSuccess(false);
@@ -906,7 +934,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
// Find the ACTIVE suggested lot (not rejected lots)
const activeSuggestedLots = sameItemLotsInExpected.filter(lot =>
const activeSuggestedLots = sameItemLotsInExpected.filter((lot: any) =>
lot.lotAvailability !== 'rejected' &&
lot.stockOutLineStatus !== 'rejected' &&
lot.stockOutLineStatus !== 'completed'
@@ -919,21 +947,78 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
return;
}
// Use the first active suggested lot as the "expected" lot
// 2) Check if scanned lot is exactly in active suggested lots
const exactLotMatch = activeSuggestedLots.find((l: any) =>
(scanned?.inventoryLotLineId && l.lotId === scanned.inventoryLotLineId) ||
(scanned?.lotNo && l.lotNo === scanned.lotNo)
);
if (exactLotMatch && scanned?.lotNo) {
// ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快)
console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`);
if (!exactLotMatch.stockOutLineId) {
console.warn("No stockOutLineId on exactLotMatch, cannot update status by QR.");
setQrScanError(true);
setQrScanSuccess(false);
return;
}
try {
const res = await updateStockOutLineStatusByQRCodeAndLotNo({
pickOrderLineId: exactLotMatch.pickOrderLineId,
inventoryLotNo: scanned.lotNo,
stockOutLineId: exactLotMatch.stockOutLineId,
itemId: exactLotMatch.itemId,
status: "checked",
});
if (res.code === "checked" || res.code === "SUCCESS") {
setQrScanError(false);
setQrScanSuccess(true);
// ✅ 刷新数据而不是直接更新 state
await fetchJobOrderData();
console.log("✅ Status updated, data refreshed");
} else if (res.code === "LOT_NUMBER_MISMATCH") {
console.warn("Backend reported LOT_NUMBER_MISMATCH:", res.message);
setQrScanError(true);
setQrScanSuccess(false);
} else if (res.code === "ITEM_MISMATCH") {
console.warn("Backend reported ITEM_MISMATCH:", res.message);
setQrScanError(true);
setQrScanSuccess(false);
} else {
console.warn("Unexpected response code from backend:", res.code);
setQrScanError(true);
setQrScanSuccess(false);
}
} catch (e) {
console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e);
setQrScanError(true);
setQrScanSuccess(false);
}
return; // ✅ 直接返回,不再调用 handleQrCodeSubmit
}
// Case 2: Item matches but lot number differs -> open confirmation modal
const expectedLot = activeSuggestedLots[0];
if (!expectedLot) {
console.error("Could not determine expected lot for confirmation");
setQrScanError(true);
setQrScanSuccess(false);
return;
}
// 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}`);
// 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}`);
await handleQrCodeSubmit(scanned.lotNo);
return;
}
// 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
setSelectedLotForQr(expectedLot);
handleLotMismatch(
{
@@ -955,7 +1040,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
setQrScanSuccess(false);
return;
}
}, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen]);
}, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, fetchJobOrderData]);


const handleManualInputSubmit = useCallback(() => {
@@ -1229,7 +1314,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}, [fetchJobOrderData, checkAndAutoAssignNext]);
const handleSubmitAllScanned = useCallback(async () => {
const scannedLots = combinedLotData.filter(lot =>
lot.stockOutLineStatus === 'checked' // Only submit items that are scanned but not yet submitted
lot.stockOutLineStatus === 'checked'
);
if (scannedLots.length === 0) {
@@ -1238,62 +1323,53 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
setIsSubmittingAll(true);
console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`);
console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
try {
// Submit all items in parallel using Promise.all
const submitPromises = scannedLots.map(async (lot) => {
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
// 转换为 batchSubmitList 所需的格式
const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0;
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
if (cumulativeQty >= (lot.requiredQty || 0)) {
newStatus = 'completed';
}
console.log(`Submitting lot ${lot.lotNo}: qty=${cumulativeQty}, status=${newStatus}`);
// Update stock out line
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: newStatus,
qty: cumulativeQty
});
// Update inventory
if (submitQty > 0) {
await updateInventoryLotLineQuantities({
inventoryLotLineId: lot.lotId,
qty: submitQty,
status: 'available',
operation: 'pick'
});
}
// Check if pick order is completed
if (newStatus === 'completed' && lot.pickOrderConsoCode) {
await checkAndCompletePickOrderByConsoCode(lot.pickOrderConsoCode);
}
return { success: true, lotNo: lot.lotNo };
return {
stockOutLineId: Number(lot.stockOutLineId) || 0,
pickOrderLineId: Number(lot.pickOrderLineId),
inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0),
actualPickQty: Number(cumulativeQty),
stockOutLineStatus: newStatus,
pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
noLot: Boolean(false) // Job Order 通常都有 lot
};
});
// Wait for all submissions to complete
const results = await Promise.all(submitPromises);
const successCount = results.filter(r => r.success).length;
const request: batchSubmitListRequest = {
userId: currentUserId || 0,
lines: lines
};
console.log(` Batch submit completed: ${successCount}/${scannedLots.length} items submitted`);
// 使用 batchSubmitList API
const result = await batchSubmitList(request);
console.log(`📥 Batch submit result:`, result);
// Refresh data once after all submissions
await fetchJobOrderData();
// 刷新数据
await fetchJobOrderData(); // 或 pickOrderId,根据页面
if (successCount > 0) {
if (result && result.code === "SUCCESS") {
setQrScanSuccess(true);
setTimeout(() => {
setQrScanSuccess(false);
checkAndAutoAssignNext();
}, 2000);
} else {
console.error("Batch submit failed:", result);
setQrScanError(true);
}
} catch (error) {
@@ -1302,7 +1378,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} finally {
setIsSubmittingAll(false);
}
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]);
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId]);

// Calculate scanned items count
const scannedItemsCount = useMemo(() => {
@@ -1409,38 +1485,8 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
},
];

const handleSearch = useCallback((query: Record<string, any>) => {
setSearchQuery({ ...query });
console.log("Search query:", query);

if (!originalCombinedData) return;

const filtered = originalCombinedData.filter((lot: any) => {
const pickOrderCodeMatch = !query.pickOrderCode ||
lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
const itemCodeMatch = !query.itemCode ||
lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
const itemNameMatch = !query.itemName ||
lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
const lotNoMatch = !query.lotNo ||
lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
});
setCombinedLotData(filtered);
console.log("Filtered lots count:", filtered.length);
}, [originalCombinedData]);

const handleReset = useCallback(() => {
setSearchQuery({});
if (originalCombinedData) {
setCombinedLotData(originalCombinedData);
}
}, [originalCombinedData]);

const handlePageChange = useCallback((event: unknown, newPage: number) => {
setPaginationController(prev => ({


+ 138
- 211
src/components/Jodetail/newJobPickExecution.tsx View File

@@ -34,7 +34,11 @@ import {
checkPickOrderCompletion,
PickOrderCompletionResponse,
checkAndCompletePickOrderByConsoCode,
confirmLotSubstitution
confirmLotSubstitution,
updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加
batchSubmitList, // ✅ 添加
batchSubmitListRequest, // ✅ 添加
batchSubmitListLineRequest,
} from "@/app/api/pickOrder/actions";
// 修改:使用 Job Order API
import {
@@ -42,7 +46,8 @@ import {
//fetchUnassignedJobOrderPickOrders,
assignJobOrderPickOrder,
fetchJobOrderLotsHierarchicalByPickOrderId,
updateJoPickOrderHandledBy
updateJoPickOrderHandledBy,
JobOrderLotsHierarchicalResponse,
} from "@/app/api/jo/actions";
import { fetchNameList, NameList } from "@/app/api/user/actions";
import {
@@ -326,11 +331,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const currentUserId = session?.id ? parseInt(session.id) : undefined;
// 修改:使用 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);
@@ -343,7 +346,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const [qrScanInput, setQrScanInput] = useState<string>('');
const [qrScanError, setQrScanError] = useState<boolean>(false);
const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null);
const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({});

@@ -376,7 +379,52 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false);
const getAllLotsFromHierarchical = useCallback((
data: JobOrderLotsHierarchicalResponse | null
): any[] => {
if (!data || !data.pickOrder || !data.pickOrderLines) {
return [];
}

const allLots: any[] = [];
data.pickOrderLines.forEach((line) => {
if (line.lots && line.lots.length > 0) {
line.lots.forEach((lot) => {
allLots.push({
...lot,
pickOrderLineId: line.id,
itemId: line.itemId,
itemCode: line.itemCode,
itemName: line.itemName,
uomCode: line.uomCode,
uomDesc: line.uomDesc,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
jobOrderId: data.pickOrder.jobOrder.id,
jobOrderCode: data.pickOrder.jobOrder.code,
// 添加 pickOrder 信息(如果需要)
pickOrderId: data.pickOrder.id,
pickOrderCode: data.pickOrder.code,
pickOrderConsoCode: data.pickOrder.consoCode,
pickOrderTargetDate: data.pickOrder.targetDate,
pickOrderType: data.pickOrder.type,
pickOrderStatus: data.pickOrder.status,
pickOrderAssignTo: data.pickOrder.assignTo,
});
});
}
});
return allLots;
}, []);
const combinedLotData = useMemo(() => {
return getAllLotsFromHierarchical(jobOrderData);
}, [jobOrderData, getAllLotsFromHierarchical]);

const originalCombinedData = useMemo(() => {
return getAllLotsFromHierarchical(jobOrderData);
}, [jobOrderData, getAllLotsFromHierarchical]);
// 修改:加载未分配的 Job Order 订单
const loadUnassignedOrders = useCallback(async () => {
setIsLoadingUnassigned(true);
@@ -467,120 +515,27 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
if (!pickOrderId) {
console.warn("⚠️ No pickOrderId provided, skipping API call");
setJobOrderData(null);
setCombinedLotData([]);
setOriginalCombinedData([]);
return;
}
console.log("🔍 Fetching job order data by pickOrderId:", pickOrderId);
window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
detail: {
hasData: false,
tabIndex: 0
}
}));
// 直接使用类型化的响应
const jobOrderData = await fetchJobOrderLotsHierarchicalByPickOrderId(pickOrderId);
console.log("✅ Job Order data:", jobOrderData);
console.log("✅ Job Order data (hierarchical):", jobOrderData);
setJobOrderData(jobOrderData);
// Transform hierarchical data to flat structure for the table
const flatLotData: any[] = [];
if (jobOrderData.pickOrder && jobOrderData.pickOrderLines) {
jobOrderData.pickOrderLines.forEach((line: any) => {
if (line.lots && line.lots.length > 0) {
line.lots.forEach((lot: any) => {
flatLotData.push({
pickOrderId: jobOrderData.pickOrder.id,
pickOrderCode: jobOrderData.pickOrder.code,
pickOrderConsoCode: jobOrderData.pickOrder.consoCode,
pickOrderTargetDate: jobOrderData.pickOrder.targetDate,
pickOrderType: jobOrderData.pickOrder.type,
pickOrderStatus: jobOrderData.pickOrder.status,
pickOrderAssignTo: jobOrderData.pickOrder.assignTo,
// Pick order line info
pickOrderLineId: line.id,
pickOrderLineRequiredQty: line.requiredQty,
pickOrderLineStatus: line.status,
// Item info
itemId: line.itemId,
itemCode: line.itemCode,
itemName: line.itemName,
uomCode: line.uomCode,
uomDesc: line.uomDesc,
// Lot info
lotId: lot.lotId,
lotNo: lot.lotNo,
expiryDate: lot.expiryDate,
location: lot.location,
availableQty: lot.availableQty,
requiredQty: lot.requiredQty,
actualPickQty: lot.actualPickQty,
lotStatus: lot.lotStatus,
lotAvailability: lot.lotAvailability,
processingStatus: lot.processingStatus,
stockOutLineId: lot.stockOutLineId,
stockOutLineStatus: lot.stockOutLineStatus,
stockOutLineQty: lot.stockOutLineQty,
suggestedPickLotId: lot.suggestedPickLotId,
// Router info
routerIndex: lot.routerIndex,
secondQrScanStatus: lot.secondQrScanStatus,
routerArea: lot.routerArea,
routerRoute: lot.routerRoute,
uomShortDesc: lot.uomShortDesc
});
});
}
});
}
console.log("✅ Transformed flat lot data:", flatLotData);
setCombinedLotData(flatLotData);
setOriginalCombinedData(flatLotData);
const hasData = flatLotData.length > 0;
window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
detail: {
hasData: hasData,
tabIndex: 0
}
}));
// 使用辅助函数获取所有 lots(不再扁平化)
const allLots = getAllLotsFromHierarchical(jobOrderData);
// Calculate completion status and send event
const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
lot.processingStatus === 'completed'
);
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: allCompleted,
tabIndex: 0
}
}));
// ... 其他逻辑保持不变 ...
} catch (error) {
console.error("❌ Error fetching job order data:", error);
setJobOrderData(null);
setCombinedLotData([]);
setOriginalCombinedData([]);
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
tabIndex: 0
}
}));
} finally {
setCombinedDataLoading(false);
}
}, []);
}, [getAllLotsFromHierarchical]);
const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => {
if (!currentUserId || !pickOrderId || !itemId) {
return;
@@ -796,7 +751,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
pickOrderLineId: selectedLotForQr.pickOrderLineId,
stockOutLineId: selectedLotForQr.stockOutLineId,
originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId,
newInventoryLotLineId: newLotLineId
newInventoryLotNo: scannedLotData.lotNo
});
console.log(" Lot substitution result:", substitutionResult);
@@ -947,10 +902,53 @@ 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}`);
await handleQrCodeSubmit(scanned.lotNo);
return;
// ✅ Case 1: 使用 updateStockOutLineStatusByQRCodeAndLotNo API(更快)
console.log(`✅ Exact lot match found for ${scanned.lotNo}, using fast API`);
if (!expectedLot.stockOutLineId) {
console.warn("No stockOutLineId on expectedLot, cannot update status by QR.");
setQrScanError(true);
setQrScanSuccess(false);
return;
}
try {
const res = await updateStockOutLineStatusByQRCodeAndLotNo({
pickOrderLineId: expectedLot.pickOrderLineId,
inventoryLotNo: scanned.lotNo,
stockOutLineId: expectedLot.stockOutLineId,
itemId: expectedLot.itemId,
status: "checked",
});
if (res.code === "checked" || res.code === "SUCCESS") {
setQrScanError(false);
setQrScanSuccess(true);
// ✅ 刷新数据而不是直接更新 state
const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
await fetchJobOrderData(pickOrderId);
console.log("✅ Status updated, data refreshed");
} else if (res.code === "LOT_NUMBER_MISMATCH") {
console.warn("Backend reported LOT_NUMBER_MISMATCH:", res.message);
setQrScanError(true);
setQrScanSuccess(false);
} else if (res.code === "ITEM_MISMATCH") {
console.warn("Backend reported ITEM_MISMATCH:", res.message);
setQrScanError(true);
setQrScanSuccess(false);
} else {
console.warn("Unexpected response code from backend:", res.code);
setQrScanError(true);
setQrScanSuccess(false);
}
} catch (e) {
console.error("Error calling updateStockOutLineStatusByQRCodeAndLotNo:", e);
setQrScanError(true);
setQrScanSuccess(false);
}
return; // ✅ 直接返回,不再调用 handleQrCodeSubmit
}
// Case 2: Same item, different lot - show confirmation modal
@@ -1255,7 +1253,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}, [fetchJobOrderData, checkAndAutoAssignNext]);
const handleSubmitAllScanned = useCallback(async () => {
const scannedLots = combinedLotData.filter(lot =>
lot.stockOutLineStatus === 'checked' // Only submit items that are scanned but not yet submitted
lot.stockOutLineStatus === 'checked'
);
if (scannedLots.length === 0) {
@@ -1264,94 +1262,54 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}
setIsSubmittingAll(true);
console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`);
console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
try {
// Submit all items in parallel using Promise.all
const submitPromises = scannedLots.map(async (lot) => {
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty;
// ✅ 转换为 batchSubmitList 所需的格式
const lines: batchSubmitListLineRequest[] = scannedLots.map((lot) => {
const submitQty = lot.requiredQty || lot.pickOrderLineRequiredQty || 0;
const currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty;
let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
if (cumulativeQty >= (lot.requiredQty || 0)) {
newStatus = 'completed';
}
console.log(`Submitting lot ${lot.lotNo}: qty=${cumulativeQty}, status=${newStatus}`);
// Update stock out line
await updateStockOutLineStatus({
id: lot.stockOutLineId,
status: newStatus,
qty: cumulativeQty
});
// Update inventory
if (submitQty > 0) {
await updateInventoryLotLineQuantities({
inventoryLotLineId: lot.lotId,
qty: submitQty,
status: 'available',
operation: 'pick'
});
}
// REMOVED: Don't check completion here - do it after all submissions
// Return the lot info for completion check
return {
success: true,
lotNo: lot.lotNo,
pickOrderConsoCode: lot.pickOrderConsoCode,
newStatus: newStatus
return {
stockOutLineId: Number(lot.stockOutLineId) || 0,
pickOrderLineId: Number(lot.pickOrderLineId),
inventoryLotLineId: lot.lotId ? Number(lot.lotId) : null,
requiredQty: Number(lot.requiredQty || lot.pickOrderLineRequiredQty || 0),
actualPickQty: Number(cumulativeQty),
stockOutLineStatus: newStatus,
pickOrderConsoCode: String(lot.pickOrderConsoCode || ''),
noLot: Boolean(false) // Job Order 通常都有 lot
};
});
// 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`);
const request: batchSubmitListRequest = {
userId: currentUserId || 0,
lines: lines
};
// FIXED: Check completion AFTER all submissions are done
// Collect unique consoCodes from completed lots
const completedConsoCodes = new Set<string>();
results.forEach(result => {
if (result.success && result.newStatus === 'completed' && result.pickOrderConsoCode) {
completedConsoCodes.add(result.pickOrderConsoCode);
}
});
// ✅ 使用 batchSubmitList API
const result = await batchSubmitList(request);
console.log(`📥 Batch submit result:`, result);
// Check completion for each unique consoCode
await Promise.all(
Array.from(completedConsoCodes).map(async (consoCode) => {
try {
console.log(`🔍 Checking completion for pick order: ${consoCode}`);
const completionResponse = await checkAndCompletePickOrderByConsoCode(consoCode);
console.log(` Pick order completion check result for ${consoCode}:`, completionResponse);
if (completionResponse.code === "SUCCESS") {
console.log(`✅ Pick order ${consoCode} completed successfully!`);
} else if (completionResponse.message === "not completed") {
console.log(`⏳ Pick order ${consoCode} not completed yet, more lines remaining`);
} else {
console.error(`❌ Error checking completion for ${consoCode}: ${completionResponse.message}`);
}
} catch (error) {
console.error(`❌ Error checking pick order completion for ${consoCode}:`, error);
}
}));
// Refresh data once after all submissions and completion checks
// 刷新数据
const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
await fetchJobOrderData(pickOrderId);
if (successCount > 0) {
if (result && result.code === "SUCCESS") {
setQrScanSuccess(true);
setTimeout(() => {
setQrScanSuccess(false);
checkAndAutoAssignNext();
}, 2000);
} else {
console.error("Batch submit failed:", result);
setQrScanError(true);
}
} catch (error) {
@@ -1360,7 +1318,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} finally {
setIsSubmittingAll(false);
}
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]);
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId])

// Calculate scanned items count
const scannedItemsCount = useMemo(() => {
@@ -1469,38 +1427,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
},
];

const handleSearch = useCallback((query: Record<string, any>) => {
setSearchQuery({ ...query });
console.log("Search query:", query);

if (!originalCombinedData) return;

const filtered = originalCombinedData.filter((lot: any) => {
const pickOrderCodeMatch = !query.pickOrderCode ||
lot.pickOrderCode?.toLowerCase().includes((query.pickOrderCode || "").toLowerCase());
const itemCodeMatch = !query.itemCode ||
lot.itemCode?.toLowerCase().includes((query.itemCode || "").toLowerCase());
const itemNameMatch = !query.itemName ||
lot.itemName?.toLowerCase().includes((query.itemName || "").toLowerCase());
const lotNoMatch = !query.lotNo ||
lot.lotNo?.toLowerCase().includes((query.lotNo || "").toLowerCase());
return pickOrderCodeMatch && itemCodeMatch && itemNameMatch && lotNoMatch;
});
setCombinedLotData(filtered);
console.log("Filtered lots count:", filtered.length);
}, [originalCombinedData]);

const handleReset = useCallback(() => {
setSearchQuery({});
if (originalCombinedData) {
setCombinedLotData(originalCombinedData);
}
}, [originalCombinedData]);

const handlePageChange = useCallback((event: unknown, newPage: number) => {
setPaginationController(prev => ({


+ 4
- 1
src/components/ProductionProcess/ProductionProcessDetail.tsx View File

@@ -620,6 +620,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
const isCompleted = statusLower === 'completed';
const isInProgress = statusLower === 'inprogress' || statusLower === 'in progress';
const isPaused = statusLower === 'paused';
const isPending = statusLower === 'pending' || status === '';
return (
@@ -657,6 +658,8 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
}} />
) : isPending ? (
<Chip label={t("Pending")} color="default" size="small" />
) : isPaused ? (
<Chip label={t("Paused")} color="warning" size="small" />
) : (
<Chip label={t("Unknown")} color="error" size="small" />
)}
@@ -672,7 +675,7 @@ const processQrCode = useCallback((qrValue: string, lineId: number) => {
>
{t("Start")}
</Button>
) : statusLower === 'in_progress' || statusLower === 'in progress' ? (
) : statusLower === 'in_progress' || statusLower === 'in progress' || statusLower === 'paused' ? (
<Button
variant="contained"
size="small"


+ 4
- 4
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx View File

@@ -131,7 +131,7 @@ const isStockSufficient = (line: JobOrderLine) => {
const stockCounts = useMemo(() => {
// 过滤掉 consumables 类型的 lines
const nonConsumablesLines = jobOrderLines.filter(
line => line.type?.toLowerCase() !== "consumables"
line => line.type?.toLowerCase() !== "consumables" && line.type?.toLowerCase() !== "cmb"
);
const total = nonConsumablesLines.length;
const sufficient = nonConsumablesLines.filter(isStockSufficient).length;
@@ -334,7 +334,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
align: "right",
headerAlign: "right",
renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
if (params.row.type?.toLowerCase() === "consumables") {
if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
return t("N/A");
}
@@ -350,7 +350,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
type: "number",
renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
// 如果是 consumables,显示 N/A
if (params.row.type?.toLowerCase() === "consumables") {
if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
return t("N/A");
}
const stockAvailable = getStockAvailable(params.row);
@@ -386,7 +386,7 @@ const handleRelease = useCallback(async ( jobOrderId: number) => {
headerAlign: "center",
type: "boolean",
renderCell: (params: GridRenderCellParams<JobOrderLine>) => {
if (params.row.type?.toLowerCase() === "consumables") {
if (params.row.type?.toLowerCase() === "consumables"|| params.row.type?.toLowerCase() === "cmb") {
return <Typography>{t("N/A")}</Typography>;
}
return isStockSufficient(params.row)


+ 49
- 4
src/components/ProductionProcess/ProductionProcessList.tsx View File

@@ -22,6 +22,9 @@ import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import {
fetchAllJoborderProductProcessInfo,
AllJoborderProductProcessInfoResponse,
updateJo,
fetchProductProcessesByJobOrderId,
completeProductProcessLine
} from "@/app/api/jo/actions";
import { StockInLineInput } from "@/app/api/stockIn";
import { PrinterCombo } from "@/app/api/settings/printer";
@@ -55,6 +58,7 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
});
setOpenModal(true);
}, [t]);

const fetchProcesses = useCallback(async () => {
setLoading(true);
try {
@@ -72,12 +76,48 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
useEffect(() => {
fetchProcesses();
}, [fetchProcesses]);
const handleUpdateJo = useCallback(async (process: AllJoborderProductProcessInfoResponse) => {
if (!process.jobOrderId) {
alert(t("Invalid Job Order Id"));
return;
}
try {
setLoading(true); // 可选:已有 loading state 可复用
// 1) 拉取该 JO 的所有 process,取出全部 lineId
const processes = await fetchProductProcessesByJobOrderId(process.jobOrderId);
const lineIds = (processes ?? [])
.flatMap(p => (p as any).productProcessLines ?? [])
.map(l => l.id)
.filter(Boolean);
// 2) 逐个调用 completeProductProcessLine
for (const lineId of lineIds) {
try {
await completeProductProcessLine(lineId);
} catch (e) {
console.error("completeProductProcessLine failed for lineId:", lineId, e);
}
}
// 3) 更新 JO 状态
await updateJo({ id: process.jobOrderId, status: "completed" });
// 4) 刷新列表
await fetchProcesses();
} catch (e) {
console.error(e);
alert(t("An error has occurred. Please try again later."));
} finally {
setLoading(false);
}
}, [t, fetchProcesses]);
const closeNewModal = useCallback(() => {
// const response = updateJo({ id: 1, status: "storing" });
setOpenModal(false); // Close the modal first
// setTimeout(() => {
// }, 300); // Add a delay to avoid immediate re-trigger of useEffect
}, []);

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

@@ -104,10 +144,10 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
? "primary"
: "default";

const finishedCount =
(process as any).finishedProductProcessLineCount ??
(process as any).FinishedProductProcessLineCount ??
0;
const finishedCount =
(process.lines || []).filter(
(l) => String(l.status ?? "").trim().toLowerCase() === "completed"
).length;

const totalCount = process.productProcessLineCount ?? process.lines?.length ?? 0;
const linesWithStatus = (process.lines || []).filter(
@@ -184,6 +224,11 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
<Button variant="contained" size="small" onClick={() => onSelectProcess(process.jobOrderId, process.id)}>
{t("View Details")}
</Button>
{statusLower !== "completed" && (
<Button variant="contained" size="small" onClick={() => handleUpdateJo(process)}>
{t("Update Job Order")}
</Button>
)}
{statusLower === "completed" && (
<Button onClick={() => handleViewStockIn(process)}>
{t("view stockin")}


+ 81
- 20
src/components/ProductionProcess/ProductionProcessStepExecution.tsx View File

@@ -11,6 +11,10 @@ import {
TableCell,
TableHead,
TableRow,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Card,
CardContent,
Grid,
@@ -21,7 +25,7 @@ import StopIcon from "@mui/icons-material/Stop";
import PauseIcon from "@mui/icons-material/Pause";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { useTranslation } from "react-i18next";
import { JobOrderProcessLineDetailResponse, updateProductProcessLineQty,updateProductProcessLineQrscan,fetchProductProcessLineDetail ,UpdateProductProcessLineQtyRequest} from "@/app/api/jo/actions";
import { JobOrderProcessLineDetailResponse, updateProductProcessLineQty,updateProductProcessLineQrscan,fetchProductProcessLineDetail ,UpdateProductProcessLineQtyRequest,saveProductProcessResumeTime,saveProductProcessIssueTime} from "@/app/api/jo/actions";
import { Operator, Machine } from "@/app/api/jo";
import React, { useCallback, useEffect, useState } from "react";
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
@@ -36,7 +40,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
lineId,
onBack,
}) => {
const { t } = useTranslation();
const { t } = useTranslation( ["common","jo"]);
const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null);
const isCompleted = lineDetail?.status === "Completed";
const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & {
@@ -71,6 +75,8 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext();
const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-";
const [remainingTime, setRemainingTime] = useState<string | null>(null);
const[isOpenReasonModel, setIsOpenReasonModel] = useState(false);
const [pauseReason, setPauseReason] = useState("");
// 检查是否两个都已扫描
//const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId;
@@ -126,6 +132,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
const timer = setInterval(update, 1000);
return () => clearInterval(timer);
}, [lineDetail?.durationInMinutes, lineDetail?.startTime]);
const handleSubmitOutput = async () => {
if (!lineDetail?.id) return;

@@ -207,22 +214,41 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
// 开始扫描

const handlePause = () => {
setIsPaused(true);
const handleOpenReasonModel = () => {
setIsOpenReasonModel(true);
setPauseReason(""); // 重置原因
};

const handleContinue = () => {
setIsPaused(false);

const handleCloseReasonModel = () => {
setIsOpenReasonModel(false);
setPauseReason(""); // 清空原因
};

const handleStop = () => {
setIsPaused(false);

// TODO: 调用停止流程的 API
const handleSaveReason = async () => {
if (!pauseReason.trim()) {
alert(t("Please enter a reason for pausing"));
return;
}
if (!lineDetail?.id) return;
try {
await saveProductProcessIssueTime({
productProcessLineId: lineDetail.id,
reason: pauseReason.trim()
});
setIsOpenReasonModel(false);
setPauseReason("");
// 刷新 line detail
fetchProductProcessLineDetail(lineDetail.id)
.then((detail) => {
setLineDetail(detail as any);
})
.catch(err => {
console.error("Failed to load line detail", err);
});
} catch (error) {
console.error("Error saving pause reason:", error);
alert(t("Failed to pause. Please try again."));
}
};

return (
<Box>
<Box sx={{ mb: 2 }}>
@@ -407,16 +433,18 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
variant="contained"
color="error"
startIcon={<StopIcon />}
onClick={handleStop}
onClick={() => saveProductProcessIssueTime(lineDetail?.id || 0 as number)}
>
{t("Stop")}
</Button>
{!isPaused ? (
*/
}
{ lineDetail?.status === 'InProgress'? (
<Button
variant="contained"
color="warning"
startIcon={<PauseIcon />}
onClick={handlePause}
onClick={() => handleOpenReasonModel()}
>
{t("Pause")}
</Button>
@@ -425,12 +453,12 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
variant="contained"
color="success"
startIcon={<PlayArrowIcon />}
onClick={handleContinue}
onClick={() => saveProductProcessResumeTime(lineDetail?.productProcessIssueId || 0 as number)}
>
{t("Continue")}
</Button>
)}
*/}
<Button
sx={{ mt: 2, alignSelf: "flex-end" }}
variant="outlined"
@@ -699,6 +727,39 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
)}
</>
)}
<Dialog
open={isOpenReasonModel}
onClose={handleCloseReasonModel}
maxWidth="sm"
fullWidth
>
<DialogTitle>{t("Pause Reason")}</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
label={t("Reason")}
fullWidth
multiline
rows={4}
value={pauseReason}
onChange={(e) => setPauseReason(e.target.value)}
//required
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseReasonModel}>
{t("Cancel")}
</Button>
<Button
onClick={handleSaveReason}
variant="contained"
disabled={!pauseReason.trim()}
>
{t("Confirm")}
</Button>
</DialogActions>
</Dialog>
</Box>
);
};


+ 9
- 1
src/i18n/zh/common.json View File

@@ -13,6 +13,8 @@
"code": "編號",
"Name": "名稱",
"Type": "類型",
"Update Job Order": "更新工單",
"No": "沒有",
"WIP": "半成品",
"R&D": "研發",
@@ -195,10 +197,16 @@
"Seq No": "加入步驟",
"Seq No Remark": "序號明細",
"Stock Available": "庫存可用",
"Confirm": "確認",
"Stock Status": "庫存狀態",
"Target Production Date": "目標生產日期",
"id": "ID",
"Finished lines": "完成行",
"Finished lines": "已完成流程",
"Please scan staff no": "請掃描員工編號",
"Paused": "已暫停",
"paused": "已暫停",
"Pause Reason": "暫停原因",
"Reason": "原因",
"Invalid Stock In Line Id": "無效庫存行ID",
"Production date": "生產日期",
"Required Qty": "需求數量",


+ 7
- 0
src/i18n/zh/jo.json View File

@@ -10,6 +10,7 @@
"Picked Qty": "已提料數量",
"Req. Qty": "需求數量",
"UoM": "銷售單位",
"No": "沒有",
"Status": "工單狀態",
"Lot No.": "批號",
"Delete Job Order": "刪除工單",
@@ -40,7 +41,13 @@
"Production Priority": "生產優先度",
"Sequence": "序",
"Item Code": "成品/半成品編號",
"Paused": "已暫停",
"paused": "已暫停",
"Pause Reason": "暫停原因",
"Reason": "原因",
"Stock Available": "倉庫可用數",
"Staff No": "員工編號",
"Please scan staff no": "請掃描員工編號",
"Stock Status": "可提料",
"Total lines: ": "所需貨品項目數量: ",
"Lines with sufficient stock: ": "可提料項目數量: ",


Loading…
Cancel
Save