浏览代码

update job, releasetype ,fast submit.

master
CANCERYS\kw093 5 天前
父节点
当前提交
a14b7f5869
共有 14 个文件被更改,包括 682 次插入475 次删除
  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 查看文件

@@ -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 }) => { export const startBatchReleaseAsync = cache(async (data: { ids: number[]; userId: number }) => {
const { ids, userId } = data; const { ids, userId } = data;
return await serverFetchJson<{ id: number|null; code: string; entity?: any }>( return await serverFetchJson<{ id: number|null; code: string; entity?: any }>(


+ 89
- 3
src/app/api/jo/actions.ts 查看文件

@@ -383,6 +383,8 @@ export interface JobOrderProcessLineDetailResponse {
byproductName: string; byproductName: string;
byproductQty: number; byproductQty: number;
byproductUom: string; byproductUom: string;
productProcessIssueId: number;
productProcessIssueStatus: string;
} }
export interface JobOrderLineInfo { export interface JobOrderLineInfo {
id: number, id: number,
@@ -453,6 +455,90 @@ export interface JobTypeResponse {
id: number; id: number;
name: string; 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) => { export const deleteJobOrder=cache(async (jobOrderId: number) => {
return serverFetchJson<any>( return serverFetchJson<any>(
`${BASE_API_URL}/jo/demo/deleteJobOrder/${jobOrderId}`, `${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) => { export const fetchJobOrderLotsHierarchicalByPickOrderId = cache(async (pickOrderId: number) => {
return serverFetchJson<any>(
return serverFetchJson<JobOrderLotsHierarchicalResponse>(
`${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order/${pickOrderId}`, `${BASE_API_URL}/jo/all-lots-hierarchical-by-pick-order/${pickOrderId}`,
{ {
method: "GET", method: "GET",
@@ -568,7 +654,7 @@ export const startProductProcessLine = async (lineId: number) => {
}; };
export const completeProductProcessLine = async (lineId: number) => { export const completeProductProcessLine = async (lineId: number) => {
return serverFetchJson<any>( return serverFetchJson<any>(
`${BASE_API_URL}/product-process/demo/ProcessLine/complete/${lineId}`,
`${BASE_API_URL}/product-process/Demo/ProcessLine/complete/${lineId}`,
{ {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -715,7 +801,7 @@ export const assignJobOrderPickOrder = async (pickOrderId: number, userId: numbe


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


+ 2
- 0
src/app/api/jo/index.ts 查看文件

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


+ 1
- 21
src/app/api/pickOrder/actions.ts 查看文件

@@ -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) => { export const fetchAllPickOrderDetails = cache(async (userId?: number) => {
if (!userId) { if (!userId) {
return { return {


+ 41
- 5
src/components/FinishedGoodSearch/FinishedGoodFloorLanePanel.tsx 查看文件

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


// 初始化 // 初始化
useEffect(() => { useEffect(() => {
@@ -168,10 +168,34 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw
{t("Day After Tomorrow")} ({getDateLabel(2)}) {t("Day After Tomorrow")} ({getDateLabel(2)})
</MenuItem> </MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Box> </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 <Box
sx={{ sx={{
p: 1, p: 1,
@@ -186,6 +210,13 @@ const FinishedGoodFloorLanePanel: React.FC<Props> = ({ onPickOrderAssigned, onSw
{t("EDT - Lane Code (Unassigned/Total)")} {t("EDT - Lane Code (Unassigned/Total)")}
</Typography> </Typography>
</Box> </Box>
</Stack>
<Stack direction="row" spacing={2} sx={{ mb: 2, alignItems: 'flex-start' }}>

</Stack> </Stack>




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




</Box> </Box>
); );
}; };


+ 14
- 18
src/components/JoSearch/JoSearch.tsx 查看文件

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


const getStockCounts = (jo: JobOrder) => { 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 { 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); const stockCounts = getStockCounts(row);
return ( return (
<span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}> <span style={{ color: stockCounts.insufficient > 0 ? 'red' : 'green' }}>
{stockCounts.sufficient}/{stockCounts.total}
{stockCounts.sufficient}/{stockCounts.sufficient + stockCounts.insufficient}
</span> </span>
); );
} }
@@ -229,17 +219,20 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT
// onClick: (record) => onDetailClick(record), // onClick: (record) => onDetailClick(record),
// buttonIcon: <EditNote />, // buttonIcon: <EditNote />,
renderCell: (row) => { renderCell: (row) => {
const btnSx = getButtonSx(row);
//const btnSx = getButtonSx(row);
return ( return (
<Button <Button
id="emailSupplier" id="emailSupplier"
type="button" type="button"
variant="contained" variant="contained"
color="primary" 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"} // disabled={params.row.status != "rejected" && params.row.status != "partially_completed"}
onClick={() => onDetailClick(row)} 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 [openModal, setOpenModal] = useState<boolean>(false);
const [modalInfo, setModalInfo] = useState<StockInLineInput>(); const [modalInfo, setModalInfo] = useState<StockInLineInput>();
/*
const onDetailClick = useCallback((record: JobOrder) => { const onDetailClick = useCallback((record: JobOrder) => {


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

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


+ 232
- 186
src/components/Jodetail/JobPickExecution.tsx 查看文件

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

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

const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
@@ -375,6 +380,60 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); 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 订单 // 修改:加载未分配的 Job Order 订单
const loadUnassignedOrders = useCallback(async () => { const loadUnassignedOrders = useCallback(async () => {
setIsLoadingUnassigned(true); setIsLoadingUnassigned(true);
@@ -466,113 +525,53 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
try { try {
const userIdToUse = userId || currentUserId; const userIdToUse = userId || currentUserId;
console.log(" fetchJobOrderData called with userId:", userIdToUse);
if (!userIdToUse) { if (!userIdToUse) {
console.warn("⚠️ No userId available, skipping API call"); console.warn("⚠️ No userId available, skipping API call");
setJobOrderData(null); setJobOrderData(null);
setCombinedLotData([]);
setOriginalCombinedData([]);
return; return;
} }
window.dispatchEvent(new CustomEvent('jobOrderDataStatus', { window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
detail: { detail: {
hasData: false, hasData: false,
tabIndex: 0 tabIndex: 0
} }
})); }));
// 使用 Job Order API
// 直接使用类型化的响应
const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse); const jobOrderData = await fetchJobOrderLotsHierarchical(userIdToUse);
console.log(" Job Order data:", jobOrderData);
console.log(" Job Order data (hierarchical):", jobOrderData);
setJobOrderData(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', { window.dispatchEvent(new CustomEvent('jobOrderDataStatus', {
detail: { detail: {
hasData: hasData, hasData: hasData,
tabIndex: 0 tabIndex: 0
} }
})); }));
// 计算完成状态并发送事件
const allCompleted = flatLotData.length > 0 && flatLotData.every((lot: any) =>
// 计算完成状态
const allCompleted = allLots.length > 0 && allLots.every((lot) =>
lot.processingStatus === 'completed' lot.processingStatus === 'completed'
); );
// 发送完成状态事件,包含标签页信息
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: { detail: {
allLotsCompleted: allCompleted, allLotsCompleted: allCompleted,
tabIndex: 0 // 明确指定这是来自标签页 0 的事件
tabIndex: 0
} }
})); }));
} catch (error) { } catch (error) {
console.error("❌ Error fetching job order data:", error); console.error("❌ Error fetching job order data:", error);
setJobOrderData(null); setJobOrderData(null);
setCombinedLotData([]);
setOriginalCombinedData([]);
// 如果加载失败,禁用打印按钮
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', { window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: { detail: {
allLotsCompleted: false, allLotsCompleted: false,
@@ -582,7 +581,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} finally { } finally {
setCombinedDataLoading(false); setCombinedDataLoading(false);
} }
}, [currentUserId]);
}, [currentUserId, getAllLotsFromHierarchical]);


// 修改:初始化时加载数据 // 修改:初始化时加载数据
useEffect(() => { useEffect(() => {
@@ -828,28 +827,70 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
setIsConfirmingLot(false); setIsConfirmingLot(false);
} }
}, [expectedLotData, scannedLotData, selectedLotForQr, fetchJobOrderData]); }, [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) => { const processOutsideQrCode = useCallback(async (latestQr: string) => {
// Don't process if confirmation modal is open
// Don't process if confirmation modal is open
if (lotConfirmationOpen) { if (lotConfirmationOpen) {
console.log("⏸️ Confirmation modal is open, skipping QR processing"); console.log("⏸️ Confirmation modal is open, skipping QR processing");
return; return;
} }

// 1) Parse JSON safely
let qrData: any = null; let qrData: any = null;
try { try {
qrData = JSON.parse(latestQr); qrData = JSON.parse(latestQr);
} catch { } 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; return;
} }

try { try {
// Only use the new API when we have JSON with stockInLineId + itemId // Only use the new API when we have JSON with stockInLineId + itemId
if (!(qrData?.stockInLineId && qrData?.itemId)) { if (!(qrData?.stockInLineId && qrData?.itemId)) {
@@ -859,18 +900,6 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
return; 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 // Call new analyze-qr-code API
const analysis = await analyzeQrCode({ const analysis = await analyzeQrCode({
itemId: qrData.itemId, itemId: qrData.itemId,
@@ -892,13 +921,12 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} = analysis || {}; } = analysis || {};
// 1) Find all lots for the same item from current expected list // 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.itemId && analyzedItemId && l.itemId === analyzedItemId) ||
(l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode) (l.itemCode && analyzedItemCode && l.itemCode === analyzedItemCode)
); );
if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) { if (!sameItemLotsInExpected || sameItemLotsInExpected.length === 0) {
// Case 3: No item code match
console.error("No item match in expected lots for scanned code"); console.error("No item match in expected lots for scanned code");
setQrScanError(true); setQrScanError(true);
setQrScanSuccess(false); setQrScanSuccess(false);
@@ -906,7 +934,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} }
// Find the ACTIVE suggested lot (not rejected lots) // Find the ACTIVE suggested lot (not rejected lots)
const activeSuggestedLots = sameItemLotsInExpected.filter(lot =>
const activeSuggestedLots = sameItemLotsInExpected.filter((lot: any) =>
lot.lotAvailability !== 'rejected' && lot.lotAvailability !== 'rejected' &&
lot.stockOutLineStatus !== 'rejected' && lot.stockOutLineStatus !== 'rejected' &&
lot.stockOutLineStatus !== 'completed' lot.stockOutLineStatus !== 'completed'
@@ -919,21 +947,78 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
return; 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]; 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); await handleQrCodeSubmit(scanned.lotNo);
return; return;
} }
// Case 2: Same item, different lot - show confirmation modal
console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`); console.log(`🔍 Lot mismatch: Expected ${expectedLot.lotNo}, Scanned ${scanned?.lotNo}`);
// DON'T stop scanning - just pause QR processing by showing modal
setSelectedLotForQr(expectedLot); setSelectedLotForQr(expectedLot);
handleLotMismatch( handleLotMismatch(
{ {
@@ -955,7 +1040,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
setQrScanSuccess(false); setQrScanSuccess(false);
return; return;
} }
}, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen]);
}, [combinedLotData, handleQrCodeSubmit, handleLotMismatch, lotConfirmationOpen, fetchJobOrderData]);




const handleManualInputSubmit = useCallback(() => { const handleManualInputSubmit = useCallback(() => {
@@ -1229,7 +1314,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}, [fetchJobOrderData, checkAndAutoAssignNext]); }, [fetchJobOrderData, checkAndAutoAssignNext]);
const handleSubmitAllScanned = useCallback(async () => { const handleSubmitAllScanned = useCallback(async () => {
const scannedLots = combinedLotData.filter(lot => 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) { if (scannedLots.length === 0) {
@@ -1238,62 +1323,53 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} }
setIsSubmittingAll(true); setIsSubmittingAll(true);
console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`);
console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
try { 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 currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty; const cumulativeQty = currentActualPickQty + submitQty;
let newStatus = 'partially_completed'; let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
if (cumulativeQty >= (lot.requiredQty || 0)) {
newStatus = 'completed'; 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); setQrScanSuccess(true);
setTimeout(() => { setTimeout(() => {
setQrScanSuccess(false); setQrScanSuccess(false);
checkAndAutoAssignNext(); checkAndAutoAssignNext();
}, 2000); }, 2000);
} else {
console.error("Batch submit failed:", result);
setQrScanError(true);
} }
} catch (error) { } catch (error) {
@@ -1302,7 +1378,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} finally { } finally {
setIsSubmittingAll(false); setIsSubmittingAll(false);
} }
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]);
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId]);


// Calculate scanned items count // Calculate scanned items count
const scannedItemsCount = useMemo(() => { 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) => { const handlePageChange = useCallback((event: unknown, newPage: number) => {
setPaginationController(prev => ({ setPaginationController(prev => ({


+ 138
- 211
src/components/Jodetail/newJobPickExecution.tsx 查看文件

@@ -34,7 +34,11 @@ import {
checkPickOrderCompletion, checkPickOrderCompletion,
PickOrderCompletionResponse, PickOrderCompletionResponse,
checkAndCompletePickOrderByConsoCode, checkAndCompletePickOrderByConsoCode,
confirmLotSubstitution
confirmLotSubstitution,
updateStockOutLineStatusByQRCodeAndLotNo, // ✅ 添加
batchSubmitList, // ✅ 添加
batchSubmitListRequest, // ✅ 添加
batchSubmitListLineRequest,
} from "@/app/api/pickOrder/actions"; } from "@/app/api/pickOrder/actions";
// 修改:使用 Job Order API // 修改:使用 Job Order API
import { import {
@@ -42,7 +46,8 @@ import {
//fetchUnassignedJobOrderPickOrders, //fetchUnassignedJobOrderPickOrders,
assignJobOrderPickOrder, assignJobOrderPickOrder,
fetchJobOrderLotsHierarchicalByPickOrderId, fetchJobOrderLotsHierarchicalByPickOrderId,
updateJoPickOrderHandledBy
updateJoPickOrderHandledBy,
JobOrderLotsHierarchicalResponse,
} from "@/app/api/jo/actions"; } from "@/app/api/jo/actions";
import { fetchNameList, NameList } from "@/app/api/user/actions"; import { fetchNameList, NameList } from "@/app/api/user/actions";
import { import {
@@ -326,11 +331,9 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const currentUserId = session?.id ? parseInt(session.id) : undefined; 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 [combinedDataLoading, setCombinedDataLoading] = useState(false);
const [originalCombinedData, setOriginalCombinedData] = useState<any[]>([]);
// 添加未分配订单状态 // 添加未分配订单状态
const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]); const [unassignedOrders, setUnassignedOrders] = useState<any[]>([]);
const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false); const [isLoadingUnassigned, setIsLoadingUnassigned] = useState(false);
@@ -343,7 +346,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
const [qrScanInput, setQrScanInput] = useState<string>(''); const [qrScanInput, setQrScanInput] = useState<string>('');
const [qrScanError, setQrScanError] = useState<boolean>(false); const [qrScanError, setQrScanError] = useState<boolean>(false);
const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false); const [qrScanSuccess, setQrScanSuccess] = useState<boolean>(false);
const [jobOrderData, setJobOrderData] = useState<JobOrderLotsHierarchicalResponse | null>(null);
const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({}); const [pickQtyData, setPickQtyData] = useState<Record<string, number>>({});
const [searchQuery, setSearchQuery] = useState<Record<string, any>>({}); 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 [processedQrCodes, setProcessedQrCodes] = useState<Set<string>>(new Set());
const [lastProcessedQr, setLastProcessedQr] = useState<string>(''); const [lastProcessedQr, setLastProcessedQr] = useState<string>('');
const [isRefreshingData, setIsRefreshingData] = useState<boolean>(false); 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 订单 // 修改:加载未分配的 Job Order 订单
const loadUnassignedOrders = useCallback(async () => { const loadUnassignedOrders = useCallback(async () => {
setIsLoadingUnassigned(true); setIsLoadingUnassigned(true);
@@ -467,120 +515,27 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
if (!pickOrderId) { if (!pickOrderId) {
console.warn("⚠️ No pickOrderId provided, skipping API call"); console.warn("⚠️ No pickOrderId provided, skipping API call");
setJobOrderData(null); setJobOrderData(null);
setCombinedLotData([]);
setOriginalCombinedData([]);
return; 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); const jobOrderData = await fetchJobOrderLotsHierarchicalByPickOrderId(pickOrderId);
console.log("✅ Job Order data:", jobOrderData);
console.log("✅ Job Order data (hierarchical):", jobOrderData);
setJobOrderData(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) { } catch (error) {
console.error("❌ Error fetching job order data:", error); console.error("❌ Error fetching job order data:", error);
setJobOrderData(null); setJobOrderData(null);
setCombinedLotData([]);
setOriginalCombinedData([]);
window.dispatchEvent(new CustomEvent('pickOrderCompletionStatus', {
detail: {
allLotsCompleted: false,
tabIndex: 0
}
}));
} finally { } finally {
setCombinedDataLoading(false); setCombinedDataLoading(false);
} }
}, []);
}, [getAllLotsFromHierarchical]);
const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => { const updateHandledBy = useCallback(async (pickOrderId: number, itemId: number) => {
if (!currentUserId || !pickOrderId || !itemId) { if (!currentUserId || !pickOrderId || !itemId) {
return; return;
@@ -796,7 +751,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
pickOrderLineId: selectedLotForQr.pickOrderLineId, pickOrderLineId: selectedLotForQr.pickOrderLineId,
stockOutLineId: selectedLotForQr.stockOutLineId, stockOutLineId: selectedLotForQr.stockOutLineId,
originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId, originalSuggestedPickLotId: selectedLotForQr.suggestedPickLotId || selectedLotForQr.lotId,
newInventoryLotLineId: newLotLineId
newInventoryLotNo: scannedLotData.lotNo
}); });
console.log(" Lot substitution result:", substitutionResult); console.log(" Lot substitution result:", substitutionResult);
@@ -947,10 +902,53 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
// 2) Check if the scanned lot matches exactly // 2) Check if the scanned lot matches exactly
if (scanned?.lotNo === expectedLot.lotNo) { 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 // Case 2: Same item, different lot - show confirmation modal
@@ -1255,7 +1253,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
}, [fetchJobOrderData, checkAndAutoAssignNext]); }, [fetchJobOrderData, checkAndAutoAssignNext]);
const handleSubmitAllScanned = useCallback(async () => { const handleSubmitAllScanned = useCallback(async () => {
const scannedLots = combinedLotData.filter(lot => 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) { if (scannedLots.length === 0) {
@@ -1264,94 +1262,54 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} }
setIsSubmittingAll(true); setIsSubmittingAll(true);
console.log(`📦 Submitting ${scannedLots.length} scanned items in parallel...`);
console.log(`📦 Submitting ${scannedLots.length} scanned items using batchSubmitList...`);
try { 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 currentActualPickQty = lot.actualPickQty || 0;
const cumulativeQty = currentActualPickQty + submitQty; const cumulativeQty = currentActualPickQty + submitQty;
let newStatus = 'partially_completed'; let newStatus = 'partially_completed';
if (cumulativeQty >= lot.requiredQty) {
if (cumulativeQty >= (lot.requiredQty || 0)) {
newStatus = 'completed'; 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; const pickOrderId = filterArgs?.pickOrderId ? Number(filterArgs.pickOrderId) : undefined;
await fetchJobOrderData(pickOrderId); await fetchJobOrderData(pickOrderId);
if (successCount > 0) {
if (result && result.code === "SUCCESS") {
setQrScanSuccess(true); setQrScanSuccess(true);
setTimeout(() => { setTimeout(() => {
setQrScanSuccess(false); setQrScanSuccess(false);
checkAndAutoAssignNext(); checkAndAutoAssignNext();
}, 2000); }, 2000);
} else {
console.error("Batch submit failed:", result);
setQrScanError(true);
} }
} catch (error) { } catch (error) {
@@ -1360,7 +1318,7 @@ const JobPickExecution: React.FC<Props> = ({ filterArgs }) => {
} finally { } finally {
setIsSubmittingAll(false); setIsSubmittingAll(false);
} }
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext]);
}, [combinedLotData, fetchJobOrderData, checkAndAutoAssignNext, currentUserId, filterArgs?.pickOrderId])


// Calculate scanned items count // Calculate scanned items count
const scannedItemsCount = useMemo(() => { 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) => { const handlePageChange = useCallback((event: unknown, newPage: number) => {
setPaginationController(prev => ({ setPaginationController(prev => ({


+ 4
- 1
src/components/ProductionProcess/ProductionProcessDetail.tsx 查看文件

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


+ 4
- 4
src/components/ProductionProcess/ProductionProcessJobOrderDetail.tsx 查看文件

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


+ 49
- 4
src/components/ProductionProcess/ProductionProcessList.tsx 查看文件

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

const fetchProcesses = useCallback(async () => { const fetchProcesses = useCallback(async () => {
setLoading(true); setLoading(true);
try { try {
@@ -72,12 +76,48 @@ const ProductProcessList: React.FC<ProductProcessListProps> = ({ onSelectProcess
useEffect(() => { useEffect(() => {
fetchProcesses(); fetchProcesses();
}, [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 closeNewModal = useCallback(() => {
// const response = updateJo({ id: 1, status: "storing" }); // const response = updateJo({ id: 1, status: "storing" });
setOpenModal(false); // Close the modal first setOpenModal(false); // Close the modal first
// setTimeout(() => { // setTimeout(() => {
// }, 300); // Add a delay to avoid immediate re-trigger of useEffect // }, 300); // Add a delay to avoid immediate re-trigger of useEffect
}, []); }, []);

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


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


+ 81
- 20
src/components/ProductionProcess/ProductionProcessStepExecution.tsx 查看文件

@@ -11,6 +11,10 @@ import {
TableCell, TableCell,
TableHead, TableHead,
TableRow, TableRow,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Card, Card,
CardContent, CardContent,
Grid, Grid,
@@ -21,7 +25,7 @@ import StopIcon from "@mui/icons-material/Stop";
import PauseIcon from "@mui/icons-material/Pause"; import PauseIcon from "@mui/icons-material/Pause";
import PlayArrowIcon from "@mui/icons-material/PlayArrow"; import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import { useTranslation } from "react-i18next"; 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 { Operator, Machine } from "@/app/api/jo";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider"; import { useQrCodeScannerContext } from "../QrCodeScannerProvider/QrCodeScannerProvider";
@@ -36,7 +40,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
lineId, lineId,
onBack, onBack,
}) => { }) => {
const { t } = useTranslation();
const { t } = useTranslation( ["common","jo"]);
const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null); const [lineDetail, setLineDetail] = useState<JobOrderProcessLineDetailResponse | null>(null);
const isCompleted = lineDetail?.status === "Completed"; const isCompleted = lineDetail?.status === "Completed";
const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & { const [outputData, setOutputData] = useState<UpdateProductProcessLineQtyRequest & {
@@ -71,6 +75,8 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext(); const { values: qrValues, startScan, stopScan, resetScan } = useQrCodeScannerContext();
const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-"; const equipmentName = (lineDetail as any)?.equipment || lineDetail?.equipmentType || "-";
const [remainingTime, setRemainingTime] = useState<string | null>(null); const [remainingTime, setRemainingTime] = useState<string | null>(null);
const[isOpenReasonModel, setIsOpenReasonModel] = useState(false);
const [pauseReason, setPauseReason] = useState("");
// 检查是否两个都已扫描 // 检查是否两个都已扫描
//const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId; //const bothScanned = lineDetail?.operatorId && lineDetail?.equipmentId;
@@ -126,6 +132,7 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
const timer = setInterval(update, 1000); const timer = setInterval(update, 1000);
return () => clearInterval(timer); return () => clearInterval(timer);
}, [lineDetail?.durationInMinutes, lineDetail?.startTime]); }, [lineDetail?.durationInMinutes, lineDetail?.startTime]);
const handleSubmitOutput = async () => { const handleSubmitOutput = async () => {
if (!lineDetail?.id) return; 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 ( return (
<Box> <Box>
<Box sx={{ mb: 2 }}> <Box sx={{ mb: 2 }}>
@@ -407,16 +433,18 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
variant="contained" variant="contained"
color="error" color="error"
startIcon={<StopIcon />} startIcon={<StopIcon />}
onClick={handleStop}
onClick={() => saveProductProcessIssueTime(lineDetail?.id || 0 as number)}
> >
{t("Stop")} {t("Stop")}
</Button> </Button>
{!isPaused ? (
*/
}
{ lineDetail?.status === 'InProgress'? (
<Button <Button
variant="contained" variant="contained"
color="warning" color="warning"
startIcon={<PauseIcon />} startIcon={<PauseIcon />}
onClick={handlePause}
onClick={() => handleOpenReasonModel()}
> >
{t("Pause")} {t("Pause")}
</Button> </Button>
@@ -425,12 +453,12 @@ const ProductionProcessStepExecution: React.FC<ProductionProcessStepExecutionPro
variant="contained" variant="contained"
color="success" color="success"
startIcon={<PlayArrowIcon />} startIcon={<PlayArrowIcon />}
onClick={handleContinue}
onClick={() => saveProductProcessResumeTime(lineDetail?.productProcessIssueId || 0 as number)}
> >
{t("Continue")} {t("Continue")}
</Button> </Button>
)} )}
*/}
<Button <Button
sx={{ mt: 2, alignSelf: "flex-end" }} sx={{ mt: 2, alignSelf: "flex-end" }}
variant="outlined" 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> </Box>
); );
}; };


+ 9
- 1
src/i18n/zh/common.json 查看文件

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


+ 7
- 0
src/i18n/zh/jo.json 查看文件

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


正在加载...
取消
保存