| @@ -149,6 +149,135 @@ export const recordSecondScanIssue = cache(async ( | |||
| }, | |||
| ); | |||
| }); | |||
| export interface ProductProcessResponse { | |||
| id: number; | |||
| productProcessCode: string; | |||
| status: string; | |||
| startTime?: string; | |||
| endTime?: string; | |||
| date: string; | |||
| bomId?: number; | |||
| jobOrderId?: number; | |||
| } | |||
| export interface ProductProcessLineResponse { | |||
| id: number; | |||
| seqNo: number; | |||
| name: string; | |||
| description?: string; | |||
| equipmentType?: string; | |||
| startTime?: string; | |||
| endTime?: string; | |||
| outputFromProcessQty?: number; | |||
| outputFromProcessUom?: string; | |||
| defectQty?: number; | |||
| scrapQty?: number; | |||
| byproductName?: string; | |||
| byproductQty?: number; | |||
| handlerId?: number; | |||
| } | |||
| export interface ProductProcessWithLinesResponse { | |||
| id: number; | |||
| productProcessCode: string; | |||
| status: string; | |||
| startTime?: string; | |||
| endTime?: string; | |||
| date: string; | |||
| lines: ProductProcessLineResponse[]; | |||
| } | |||
| export const startProductProcessLine = async (lineId: number, userId: number) => { | |||
| return serverFetchJson<ProductProcessLineResponse>( | |||
| `${BASE_API_URL}/product-process/lines/${lineId}/start?userId=${userId}`, | |||
| { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| } | |||
| ); | |||
| }; | |||
| // 查询所有 production processes | |||
| export const fetchProductProcesses = cache(async () => { | |||
| return serverFetchJson<{ content: ProductProcessResponse[] }>( | |||
| `${BASE_API_URL}/product-process`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["productProcess"] }, | |||
| } | |||
| ); | |||
| }); | |||
| // 根据 ID 查询 | |||
| export const fetchProductProcessById = cache(async (id: number) => { | |||
| return serverFetchJson<ProductProcessResponse>( | |||
| `${BASE_API_URL}/product-process/${id}`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["productProcess"] }, | |||
| } | |||
| ); | |||
| }); | |||
| // 根据 Job Order ID 查询 | |||
| export const fetchProductProcessesByJobOrderId = cache(async (jobOrderId: number) => { | |||
| return serverFetchJson<ProductProcessWithLinesResponse[]>( | |||
| `${BASE_API_URL}/product-process/by-job-order/${jobOrderId}`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["productProcess"] }, | |||
| } | |||
| ); | |||
| }); | |||
| // 获取 process 的所有 lines | |||
| export const fetchProductProcessLines = cache(async (processId: number) => { | |||
| return serverFetchJson<ProductProcessLineResponse[]>( | |||
| `${BASE_API_URL}/product-process/${processId}/lines`, | |||
| { | |||
| method: "GET", | |||
| next: { tags: ["productProcessLines"] }, | |||
| } | |||
| ); | |||
| }); | |||
| // 创建 production process | |||
| export const createProductProcess = async (data: { | |||
| bomId: number; | |||
| jobOrderId?: number; | |||
| date?: string; | |||
| }) => { | |||
| return serverFetchJson<{ id: number; productProcessCode: string; linesCreated: number }>( | |||
| `${BASE_API_URL}/product-process`, | |||
| { | |||
| method: "POST", | |||
| headers: { "Content-Type": "application/json" }, | |||
| body: JSON.stringify(data), | |||
| } | |||
| ); | |||
| }; | |||
| // 更新 line 产出数据 | |||
| export const updateLineOutput = async (lineId: number, data: { | |||
| outputQty?: number; | |||
| outputUom?: string; | |||
| defectQty?: number; | |||
| defectUom?: string; | |||
| scrapQty?: number; | |||
| scrapUom?: string; | |||
| byproductName?: string; | |||
| byproductQty?: number; | |||
| byproductUom?: string; | |||
| }) => { | |||
| return serverFetchJson<ProductProcessLineResponse>( | |||
| `${BASE_API_URL}/product-process/lines/${lineId}/output`, | |||
| { | |||
| method: "PUT", | |||
| headers: { "Content-Type": "application/json" }, | |||
| body: JSON.stringify(data), | |||
| } | |||
| ); | |||
| }; | |||
| export const updateSecondQrScanStatus = cache(async (pickOrderId: number, itemId: number, userId: number, qty: number) => { | |||
| return serverFetchJson<any>( | |||
| `${BASE_API_URL}/jo/update-match-status`, | |||
| @@ -193,8 +193,8 @@ export interface PickExecutionIssueData { | |||
| itemId: number; | |||
| itemCode: string; | |||
| itemDescription: string; | |||
| lotId: number; | |||
| lotNo: string; | |||
| lotId: number|null; | |||
| lotNo: string|null; | |||
| storeLocation: string; | |||
| requiredQty: number; | |||
| actualPickQty: number; | |||
| @@ -411,31 +411,82 @@ const GoodPickExecutionRecord: React.FC<Props> = ({ filterArgs }) => { | |||
| // ✅ 转换为平铺格式 | |||
| const flatLotData: any[] = []; | |||
| if (hierarchicalData.pickOrders && hierarchicalData.pickOrders.length > 0) { | |||
| hierarchicalData.pickOrders.forEach((po: any) => { | |||
| po.pickOrderLines?.forEach((line: any) => { | |||
| if (line.lots && line.lots.length > 0) { | |||
| line.lots.forEach((lot: any) => { | |||
| flatLotData.push({ | |||
| pickOrderCode: po.pickOrderCode, | |||
| itemCode: line.item.code, | |||
| itemName: line.item.name, | |||
| lotNo: lot.lotNo, | |||
| location: lot.location, | |||
| deliveryOrderCode: po.deliveryOrderCode, | |||
| requiredQty: lot.requiredQty, | |||
| actualPickQty: lot.actualPickQty, | |||
| processingStatus: lot.processingStatus, | |||
| stockOutLineStatus: lot.stockOutLineStatus | |||
| }); | |||
| if (hierarchicalData.pickOrders && hierarchicalData.pickOrders.length > 0) { | |||
| const toProc = (s?: string) => { | |||
| if (!s) return 'pending'; | |||
| const v = s.toLowerCase(); | |||
| if (v === 'completed' || v === 'complete') return 'completed'; | |||
| if (v === 'rejected') return 'rejected'; | |||
| if (v === 'partially_completed') return 'pending'; | |||
| return 'pending'; | |||
| }; | |||
| hierarchicalData.pickOrders.forEach((po: any) => { | |||
| po.pickOrderLines?.forEach((line: any) => { | |||
| const lineStockouts = line.stockouts || []; // ← 用“行级” stockouts | |||
| const lots = line.lots || []; | |||
| if (lots.length > 0) { | |||
| lots.forEach((lot: any) => { | |||
| // 先按该 lot 过滤出匹配的 stockouts(同 lotId) | |||
| const sos = lineStockouts.filter((so: any) => (so.lotId ?? null) === (lot.id ?? null)); | |||
| if (sos.length > 0) { | |||
| sos.forEach((so: any) => { | |||
| flatLotData.push({ | |||
| pickOrderCode: po.pickOrderCode, | |||
| itemCode: line.item?.code, | |||
| itemName: line.item?.name, | |||
| lotNo: so.lotNo || lot.lotNo, | |||
| location: so.location || lot.location, | |||
| deliveryOrderCode: po.deliveryOrderCode, | |||
| requiredQty: lot.requiredQty, | |||
| actualPickQty: (so.qty ?? lot.actualPickQty ?? 0), | |||
| processingStatus: toProc(so.status), // ← 用 stockouts.status | |||
| stockOutLineStatus: so.status, // 可选保留 | |||
| noLot: so.noLot === true | |||
| }); | |||
| } | |||
| }); | |||
| } else { | |||
| // 没有匹配的 stockouts,也要显示 lot | |||
| flatLotData.push({ | |||
| pickOrderCode: po.pickOrderCode, | |||
| itemCode: line.item?.code, | |||
| itemName: line.item?.name, | |||
| lotNo: lot.lotNo, | |||
| location: lot.location, | |||
| deliveryOrderCode: po.deliveryOrderCode, | |||
| requiredQty: lot.requiredQty, | |||
| actualPickQty: lot.actualPickQty ?? 0, | |||
| processingStatus: lot.processingStatus || 'pending', | |||
| stockOutLineStatus: lot.stockOutLineStatus || 'pending', | |||
| noLot: false | |||
| }); | |||
| } | |||
| }); | |||
| } else if (lineStockouts.length > 0) { | |||
| // ✅ lots 为空但有 stockouts(如「雞絲碗仔翅」),也要显示 | |||
| lineStockouts.forEach((so: any) => { | |||
| flatLotData.push({ | |||
| pickOrderCode: po.pickOrderCode, | |||
| itemCode: line.item?.code, | |||
| itemName: line.item?.name, | |||
| lotNo: so.lotNo || '', | |||
| location: so.location || '', | |||
| deliveryOrderCode: po.deliveryOrderCode, | |||
| requiredQty: line.requiredQty ?? 0, // 行级没有 lot 时,用行的 requiredQty | |||
| actualPickQty: (so.qty ?? 0), | |||
| processingStatus: toProc(so.status), | |||
| stockOutLineStatus: so.status, | |||
| noLot: so.noLot === true | |||
| }); | |||
| }); | |||
| } | |||
| setDetailLotData(flatLotData); | |||
| }); | |||
| }); | |||
| } | |||
| setDetailLotData(flatLotData); | |||
| // ✅ 计算完成状态 | |||
| const allCompleted = flatLotData.length > 0 && flatLotData.every(lot => | |||
| @@ -593,8 +644,8 @@ if (showDetailView && selectedDoPickOrder) { | |||
| <TableCell align="right">{lot.requiredQty || 0}</TableCell> | |||
| <TableCell align="right">{lot.actualPickQty || 0}</TableCell> | |||
| <TableCell align="center"> | |||
| <Chip | |||
| label={t(lot.processingStatus || 'unknown')} | |||
| <Chip | |||
| label={t(lot.processingStatus || 'unknown')} | |||
| color={lot.processingStatus === 'completed' ? 'success' : 'default'} | |||
| size="small" | |||
| /> | |||