Files
vexplor/frontend/lib/api/outsourcePurchase.ts
kjs b4346b5617 Implement Outsourcing Purchase Status Pages and Detail Modal for COMPANY_10, COMPANY_16, COMPANY_28, and COMPANY_29
- Created a detailed modal for viewing outsourcing purchase order details, including basic information and status rows for processes, vendors, and shipping vs. receiving.
- Developed an integrated status page for outsourcing purchase orders, featuring a dynamic search filter and a comprehensive table displaying order, process, vendor, and material information.
- Implemented Excel export functionality for the integrated status page, allowing users to download order data easily.

(TASK: ERP-025)
2026-05-08 09:59:23 +09:00

438 lines
15 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 외주발주관리 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: [] };
}
/**
* 외주발주번호 미리보기 — 등록된 채번 규칙 기반. 시퀀스 증가 없음.
* 응답이 빈 문자열이면 채번 규칙 미설정 — 백엔드 폴백(OPO-YYYY-NNN) 사용됨.
*/
export async function previewOrderNo(): Promise<string> {
try {
const res = await apiClient.get<ApiEnvelope<{ generatedCode: string }>>(
"/outsource-purchase/preview-order-no",
);
return res.data?.data?.generatedCode || "";
} catch {
return "";
}
}
/**
* 공정 자재투입(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 || [];
}
// ─────────────────────────────────────────────────────────────────────────────
// 외주발주현황 (TASK:ERP-025) — 발주 × 공정 × 자재 × 출고 × 입고 통합 펼친 뷰
// ─────────────────────────────────────────────────────────────────────────────
export interface OrderStatusFilter {
keyword?: string;
source_type?: string;
order_status?: string;
release_status?: string;
date_from?: string;
date_to?: string;
}
export interface OrderStatusRow {
order_id: string;
order_no: string;
source_type: string;
source_no: string;
item_code: string;
item_name: string;
spec: string;
material: string;
quantity: number;
order_date: string;
due_date: string;
order_status: string;
manager: string;
process_id: string;
seq: number | null;
process_name: string;
vendor_code: string;
vendor_name: string;
material_needed: boolean;
material_id: string;
material_item_code: string;
material_item_name: string;
material_qty: number;
material_unit: string;
material_release_status: string;
outbound_id: string;
outbound_number: string;
outbound_status: string;
released_qty: number;
request_date: string;
received_qty: number;
remain_qty: number;
rate_pct: number;
release_status: string; // '출고완료' | '입고완료' | ''
}
export interface OrderStatusResponse {
rows: OrderStatusRow[];
total: number;
}
/** 외주발주현황 조회 (TASK:ERP-025) */
export async function listOutsourcePurchaseStatus(
filter: OrderStatusFilter = {},
): Promise<OrderStatusResponse> {
const params: Record<string, any> = {};
for (const [k, v] of Object.entries(filter)) {
if (v != null && v !== "") params[k] = v;
}
const res = await apiClient.get<ApiEnvelope<OrderStatusResponse>>(
"/outsource-purchase/status",
{ params },
);
return res.data?.data || { rows: [], total: 0 };
}