- Added a new endpoint to retrieve process materials based on routing details and work order ID. - Introduced the `getProcessMaterials` function in the `outsourcePurchaseController` to handle the logic for fetching materials. - Updated the `outsourcePurchaseRoutes` to include the new route for process materials. - Enhanced the `RegistrationModal` component to toggle material needs and automatically fill materials when required. (TASK:ERP-019)
353 lines
12 KiB
TypeScript
353 lines
12 KiB
TypeScript
/**
|
|
* 외주발주관리 API 클라이언트 (TASK:ERP-019)
|
|
* 백엔드 라우트: /api/outsource-purchase
|
|
*
|
|
* 컨트롤러:
|
|
* GET / 목록
|
|
* GET /auto-processes 공정 자동표기
|
|
* GET /:id 상세
|
|
* POST / 등록
|
|
* PUT /:id 수정
|
|
* DELETE /:id 삭제
|
|
* POST /release-request 사급자재 출고요청
|
|
*/
|
|
|
|
import { apiClient } from "@/lib/api/client";
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 타입
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export type OPOSourceType = "수주" | "작업지시" | "품목정보" | "생산계획";
|
|
export type OPOStatus = "등록" | "진행중" | "완료" | "취소" | "출고요청";
|
|
export type ReleaseStatus = "대기" | "요청" | "완료";
|
|
|
|
export interface OPOMaterial {
|
|
id?: string;
|
|
item_code?: string;
|
|
item_name?: string;
|
|
qty?: number | string;
|
|
unit?: string;
|
|
release_status?: ReleaseStatus;
|
|
release_date?: string | null;
|
|
outbound_id?: string | null;
|
|
}
|
|
|
|
export interface OPOProcess {
|
|
id?: string;
|
|
seq: number;
|
|
process_code?: string;
|
|
process_name?: string;
|
|
/** subcontractor_mng.id — 그룹핑·출고요청 키 */
|
|
vendor_id?: string;
|
|
/** 표시·하위호환용. 그룹핑 키로 사용하지 말 것 (사람이 바꿀 수 있음) */
|
|
vendor_code?: string;
|
|
vendor_name?: string;
|
|
material_needed?: boolean;
|
|
remark?: string;
|
|
materials?: OPOMaterial[];
|
|
/** 라우팅에 매핑된 외주사 후보 (드롭다운을 이 목록으로 제한) */
|
|
vendor_options?: { id: string; code: string; name: string }[];
|
|
/** item_routing_detail.id (사급자재 자동 채움 호출에 사용) */
|
|
routing_detail_id?: string;
|
|
}
|
|
|
|
export interface OPOMaster {
|
|
id?: string;
|
|
company_code?: string;
|
|
order_no?: string;
|
|
source_type: string;
|
|
source_no?: string;
|
|
item_code?: string;
|
|
item_name?: string;
|
|
spec?: string;
|
|
material?: string;
|
|
quantity?: number | string;
|
|
order_date?: string | null;
|
|
due_date?: string | null;
|
|
manager?: string;
|
|
memo?: string;
|
|
status?: OPOStatus;
|
|
writer?: string;
|
|
created_date?: string;
|
|
updated_date?: string;
|
|
// 목록 집계 필드
|
|
process_count?: number;
|
|
vendor_unassigned?: number;
|
|
material_count?: number;
|
|
material_pending?: number;
|
|
first_process_name?: string;
|
|
first_vendor_name?: string;
|
|
}
|
|
|
|
export interface OPODetail extends OPOMaster {
|
|
processes: OPOProcess[];
|
|
}
|
|
|
|
export interface OPOInputPayload {
|
|
order_no?: string;
|
|
source_type: string;
|
|
source_no?: string;
|
|
item_code?: string;
|
|
item_name?: string;
|
|
spec?: string;
|
|
material?: string;
|
|
quantity?: number | string;
|
|
order_date?: string | null;
|
|
due_date?: string | null;
|
|
manager?: string;
|
|
memo?: string;
|
|
status?: OPOStatus;
|
|
processes?: OPOProcess[];
|
|
}
|
|
|
|
export interface ListParams {
|
|
keyword?: string;
|
|
status?: string;
|
|
source_type?: string;
|
|
date_from?: string;
|
|
date_to?: string;
|
|
page?: number;
|
|
size?: number;
|
|
sort?: string;
|
|
}
|
|
|
|
export interface ListResponse {
|
|
rows: OPOMaster[];
|
|
total: number;
|
|
page: number;
|
|
size: number;
|
|
}
|
|
|
|
export interface AutoProcessRouting {
|
|
id: string;
|
|
seq_no: number | string;
|
|
process_code: string;
|
|
process_name?: string;
|
|
work_type?: string;
|
|
execution_type?: string;
|
|
outsource_supplier?: string;
|
|
standard_time?: string | number;
|
|
/** 작업지시 라우팅(work_order_process)에서 채워지는 필드 */
|
|
wo_id?: string;
|
|
plan_qty?: string | number;
|
|
routing_detail_id?: string;
|
|
result_status?: string;
|
|
}
|
|
|
|
export interface AutoProcessResponse {
|
|
/** 데이터 출처 — 'work_order' (작업지시 라우팅) | 'item_routing' (품목 라우팅 폴백) | 'none' */
|
|
source?: "work_order" | "item_routing" | "none";
|
|
routing: AutoProcessRouting[];
|
|
candidates: AutoProcessRouting[];
|
|
}
|
|
|
|
export interface OutsourceableWorkOrderRow {
|
|
id: string;
|
|
work_instruction_no: string;
|
|
qty?: string | number;
|
|
status?: string;
|
|
progress_status?: string;
|
|
item_id?: string;
|
|
start_date?: string;
|
|
end_date?: string;
|
|
created_date?: string;
|
|
/** 외주발주 가능한 첫 공정명 */
|
|
next_process_name?: string;
|
|
next_process_seq?: string | number;
|
|
/** 외주/선택가능 연속 공정 개수 */
|
|
outsourceable_count?: number;
|
|
}
|
|
|
|
export interface OutsourceableListResponse {
|
|
rows: OutsourceableWorkOrderRow[];
|
|
total: number;
|
|
page: number;
|
|
size: number;
|
|
}
|
|
|
|
export interface ReleaseRequestPayload {
|
|
material_ids: string[];
|
|
warehouse_code?: string;
|
|
memo?: string;
|
|
}
|
|
|
|
export interface ReleaseRequestResult {
|
|
outbound_ids: string[];
|
|
material_ids: string[];
|
|
outbound_count: number;
|
|
material_count: number;
|
|
}
|
|
|
|
interface ApiEnvelope<T> {
|
|
success: boolean;
|
|
data?: T;
|
|
message?: string;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// API 호출
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/** 외주발주 목록 조회 */
|
|
export async function listOutsourcePurchaseOrders(params: ListParams = {}): Promise<ListResponse> {
|
|
const res = await apiClient.get<ApiEnvelope<ListResponse>>("/outsource-purchase", { params });
|
|
return res.data?.data || { rows: [], total: 0, page: 1, size: 50 };
|
|
}
|
|
|
|
/** 외주발주 상세 조회 (마스터+공정+자재) */
|
|
export async function getOutsourcePurchaseOrder(id: string): Promise<OPODetail | null> {
|
|
const res = await apiClient.get<ApiEnvelope<OPODetail>>(`/outsource-purchase/${id}`);
|
|
return res.data?.data || null;
|
|
}
|
|
|
|
/** 외주발주 등록 */
|
|
export async function createOutsourcePurchaseOrder(payload: OPOInputPayload): Promise<OPODetail> {
|
|
const res = await apiClient.post<ApiEnvelope<OPODetail>>("/outsource-purchase", payload);
|
|
if (!res.data?.success) throw new Error(res.data?.message || "외주발주 등록 실패");
|
|
return res.data.data!;
|
|
}
|
|
|
|
/** 외주발주 수정 */
|
|
export async function updateOutsourcePurchaseOrder(id: string, payload: OPOInputPayload): Promise<OPODetail> {
|
|
const res = await apiClient.put<ApiEnvelope<OPODetail>>(`/outsource-purchase/${id}`, payload);
|
|
if (!res.data?.success) throw new Error(res.data?.message || "외주발주 수정 실패");
|
|
return res.data.data!;
|
|
}
|
|
|
|
/** 외주발주 삭제 */
|
|
export async function deleteOutsourcePurchaseOrder(id: string): Promise<{ id: string; order_no: string }> {
|
|
const res = await apiClient.delete<ApiEnvelope<{ id: string; order_no: string }>>(
|
|
`/outsource-purchase/${id}`,
|
|
);
|
|
if (!res.data?.success) throw new Error(res.data?.message || "외주발주 삭제 실패");
|
|
return res.data.data!;
|
|
}
|
|
|
|
/**
|
|
* 공정 자동표기 (TASK:ERP-019 재구현)
|
|
*
|
|
* 우선순위:
|
|
* 1) work_order_id 가 있으면 → work_order_process 기준 (작업지시별 라우팅 + 진행상황)
|
|
* 2) work_order_id 가 없거나 작업지시 라우팅이 비어있으면 → item_code 기반 폴백
|
|
*
|
|
* 둘 중 하나는 반드시 제공해야 한다.
|
|
*/
|
|
export async function getAutoProcesses(
|
|
args: { workOrderId?: string; itemCode?: string },
|
|
): Promise<AutoProcessResponse> {
|
|
const params: Record<string, string> = {};
|
|
if (args.workOrderId) params.work_order_id = args.workOrderId;
|
|
if (args.itemCode) params.item_code = args.itemCode;
|
|
if (!params.work_order_id && !params.item_code) {
|
|
return { source: "none", routing: [], candidates: [] };
|
|
}
|
|
const res = await apiClient.get<ApiEnvelope<AutoProcessResponse>>(
|
|
"/outsource-purchase/auto-processes",
|
|
{ params },
|
|
);
|
|
return res.data?.data || { source: "none", routing: [], candidates: [] };
|
|
}
|
|
|
|
/**
|
|
* 공정 자재투입(material_input) 자동 채움 — 사급자재 체크 시 호출
|
|
* 응답: 외주발주 자재 입력 형식 배열 [{item_code, item_name, qty, unit}]
|
|
*/
|
|
export async function getProcessMaterials(
|
|
args: { workOrderId?: string; routingDetailId: string },
|
|
): Promise<Array<{ item_code: string; item_name: string; qty: string; unit: string }>> {
|
|
if (!args.routingDetailId) return [];
|
|
const params: Record<string, string> = { routing_detail_id: args.routingDetailId };
|
|
if (args.workOrderId) params.work_order_id = args.workOrderId;
|
|
const res = await apiClient.get<ApiEnvelope<Array<{ item_code: string; item_name: string; qty: string; unit: string }>>>(
|
|
"/outsource-purchase/process-materials",
|
|
{ params },
|
|
);
|
|
return res.data?.data || [];
|
|
}
|
|
|
|
/**
|
|
* 외주발주 가능 작업지시 목록 (TASK:ERP-019 재구현)
|
|
* "이전 공정 완료 + 다음 공정 외주/선택가능" 작업지시만 반환
|
|
*/
|
|
export async function listOutsourceableWorkOrders(
|
|
params: { keyword?: string; page?: number; size?: number } = {},
|
|
): Promise<OutsourceableListResponse> {
|
|
const res = await apiClient.get<ApiEnvelope<OutsourceableListResponse>>(
|
|
"/outsource-purchase/outsourceable-work-orders",
|
|
{ params },
|
|
);
|
|
return res.data?.data || { rows: [], total: 0, page: 1, size: 50 };
|
|
}
|
|
|
|
/** 사급자재 출고요청 (선택된 자재 ID 배열) */
|
|
export async function requestMaterialRelease(
|
|
payload: ReleaseRequestPayload,
|
|
): Promise<ReleaseRequestResult> {
|
|
const res = await apiClient.post<ApiEnvelope<ReleaseRequestResult>>(
|
|
"/outsource-purchase/release-request",
|
|
payload,
|
|
);
|
|
if (!res.data?.success) throw new Error(res.data?.message || "사급자재 출고요청 실패");
|
|
return res.data.data!;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// 보조 마스터 조회 (공통 table-management API 활용)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
/** 외주업체 마스터 조회 (subcontractor_mng) — 기존 외주사 활용 */
|
|
export async function listSubcontractors(): Promise<any[]> {
|
|
const res = await apiClient.post(`/table-management/tables/subcontractor_mng/data`, {
|
|
page: 1,
|
|
size: 0,
|
|
autoFilter: true,
|
|
});
|
|
return res.data?.data?.data || res.data?.data?.rows || [];
|
|
}
|
|
|
|
/** 수주 목록 조회 (sales_order_mng) — 등록 모달 좌측 소스 */
|
|
export async function listSalesOrdersForOPO(keyword?: string): Promise<any[]> {
|
|
const filters: any[] = [];
|
|
if (keyword) {
|
|
filters.push({ columnName: "order_no", operator: "contains", value: keyword });
|
|
}
|
|
const res = await apiClient.post(`/table-management/tables/sales_order_mng/data`, {
|
|
page: 1,
|
|
size: 0,
|
|
dataFilter: filters.length ? { enabled: true, filters } : undefined,
|
|
autoFilter: true,
|
|
});
|
|
return res.data?.data?.data || res.data?.data?.rows || [];
|
|
}
|
|
|
|
/** 작업지시 목록 조회 (work_instruction) — 등록 모달 좌측 소스 */
|
|
export async function listWorkInstructionsForOPO(keyword?: string): Promise<any[]> {
|
|
const filters: any[] = [];
|
|
if (keyword) {
|
|
filters.push({ columnName: "work_instruction_no", operator: "contains", value: keyword });
|
|
}
|
|
const res = await apiClient.post(`/table-management/tables/work_instruction/data`, {
|
|
page: 1,
|
|
size: 0,
|
|
dataFilter: filters.length ? { enabled: true, filters } : undefined,
|
|
autoFilter: true,
|
|
});
|
|
return res.data?.data?.data || res.data?.data?.rows || [];
|
|
}
|
|
|
|
/** 품목 마스터 조회 (item_info) — 등록 모달 좌측 소스 */
|
|
export async function listItemsForOPO(keyword?: string): Promise<any[]> {
|
|
const filters: any[] = [];
|
|
if (keyword) {
|
|
filters.push({ columnName: "item_code", operator: "contains", value: keyword });
|
|
}
|
|
const res = await apiClient.post(`/table-management/tables/item_info/data`, {
|
|
page: 1,
|
|
size: 0,
|
|
dataFilter: filters.length ? { enabled: true, filters } : undefined,
|
|
autoFilter: true,
|
|
});
|
|
return res.data?.data?.data || res.data?.data?.rows || [];
|
|
}
|