Files
sam-react-prod/src/components/orders/actions.ts
kent b9f0e24950 feat(WEB): 생산지시 공정관리 연동 및 견적번호 버그 수정
- 생산지시 페이지에 공정관리 API 연동
  - getProcessList API로 사용중 공정 목록 로드
  - 품목-공정 매칭 함수 추가 (classificationRules 기반)
  - 하드코딩된 DEFAULT_PROCESSES 제거, API 데이터로 대체
  - workSteps 없을 시 안내 메시지 표시

- 수주 등록 시 quote_id 미전달 버그 수정
  - transformFrontendToApi에 quote_id 변환 로직 추가
  - 견적 선택 후 수주 등록 시 견적번호 정상 표시
2026-01-12 17:19:14 +09:00

1054 lines
32 KiB
TypeScript

'use server';
import { serverFetch } from '@/lib/api/fetch-wrapper';
// ============================================================================
// API 타입 정의
// ============================================================================
interface ApiOrder {
id: number;
tenant_id: number;
quote_id: number | null;
order_no: string;
order_type_code: string;
status_code: string;
category_code: string | null;
client_id: number | null;
client_name: string | null;
client_contact: string | null;
site_name: string | null;
quantity: number;
supply_amount: number;
tax_amount: number;
total_amount: number;
discount_rate: number;
discount_amount: number;
delivery_date: string | null;
delivery_method_code: string | null;
received_at: string | null;
memo: string | null;
remarks: string | null;
note: string | null;
created_by: number | null;
updated_by: number | null;
created_at: string;
updated_at: string;
client?: ApiClient | null;
items?: ApiOrderItem[];
quote?: ApiQuote | null;
}
interface ApiOrderItem {
id: number;
order_id: number;
item_id: number | null;
item_name: string;
specification: string | null;
quantity: number;
unit: string | null;
unit_price: number;
supply_amount: number;
tax_amount: number;
total_amount: number;
sort_order: number;
}
interface ApiClient {
id: number;
name: string;
business_no?: string;
representative?: string;
phone?: string;
email?: string;
}
interface ApiQuote {
id: number;
quote_no: string;
quote_number?: string;
site_name: string | null;
}
// 견적 목록 조회용 상세 타입
interface ApiQuoteForSelect {
id: number;
quote_number: string;
registration_date: string;
status: string;
client_id: number | null;
client_name: string | null;
site_name: string | null;
supply_amount: number;
tax_amount: number;
total_amount: number;
item_count?: number;
author?: string | null;
manager?: string | null;
contact?: string | null;
client?: {
id: number;
name: string;
contact_person?: string; // 담당자
phone?: string;
} | null;
items?: ApiQuoteItem[];
}
interface ApiQuoteItem {
id: number;
item_code?: string;
item_name: string;
type_code?: string;
symbol?: string;
specification?: string;
// QuoteItem 모델 필드명 (calculated_quantity, total_price)
calculated_quantity?: number;
quantity?: number; // fallback
unit?: string;
unit_price: number;
total_price?: number;
// 수주 품목에서 사용하는 필드명
supply_amount?: number;
tax_amount?: number;
total_amount?: number;
}
interface ApiWorkOrder {
id: number;
tenant_id: number;
work_order_no: string;
sales_order_id: number;
project_name: string | null;
process_type: string;
status: string;
assignee_id: number | null;
team_id: number | null;
scheduled_date: string | null;
memo: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
assignee?: { id: number; name: string } | null;
team?: { id: number; name: string } | null;
}
interface ApiProductionOrderResponse {
work_order: ApiWorkOrder;
order: ApiOrder;
}
interface ApiOrderStats {
total: number;
draft: number;
confirmed: number;
in_progress: number;
completed: number;
cancelled: number;
total_amount: number;
confirmed_amount: number;
}
interface ApiResponse<T> {
success: boolean;
message: string;
data: T;
}
interface PaginatedResponse<T> {
current_page: number;
data: T[];
last_page: number;
per_page: number;
total: number;
}
// ============================================================================
// Frontend 타입 정의
// ============================================================================
// 수주 상태 타입 (API와 매핑)
export type OrderStatus =
| 'order_registered' // DRAFT
| 'order_confirmed' // CONFIRMED
| 'production_ordered' // IN_PROGRESS
| 'in_production' // IN_PROGRESS (세부)
| 'rework' // IN_PROGRESS (세부)
| 'work_completed' // IN_PROGRESS (세부)
| 'shipped' // COMPLETED
| 'cancelled'; // CANCELLED
export interface Order {
id: string;
lotNumber: string; // order_no
quoteNumber: string; // quote.quote_no
quoteId?: number;
orderDate: string; // received_at
client: string; // client_name
clientId?: number;
siteName: string; // site_name
status: OrderStatus;
statusCode: string; // 원본 status_code
expectedShipDate?: string; // delivery_date
deliveryMethod?: string; // delivery_method_code
amount: number; // total_amount
supplyAmount: number;
taxAmount: number;
itemCount: number; // items.length
hasReceivable?: boolean; // 미수 여부 (추후 구현)
memo?: string;
remarks?: string;
note?: string;
items?: OrderItem[];
// 상세 페이지용 추가 필드
manager?: string; // 담당자
contact?: string; // 연락처 (client_contact)
deliveryRequestDate?: string; // 납품요청일
shippingCost?: string; // 운임비용
receiver?: string; // 수신자
receiverContact?: string; // 수신처 연락처
address?: string; // 수신처 주소
addressDetail?: string; // 상세주소
subtotal?: number; // 소계 (supply_amount와 동일)
discountRate?: number; // 할인율
totalAmount?: number; // 총금액 (amount와 동일하지만 명시적)
}
export interface OrderItem {
id: string;
itemId?: number;
itemCode?: string; // 품목코드
itemName: string;
specification?: string;
spec?: string; // specification alias
type?: string; // 층 (layer)
symbol?: string; // 부호
quantity: number;
unit?: string;
unitPrice: number;
supplyAmount: number;
taxAmount: number;
totalAmount: number;
amount?: number; // totalAmount alias
sortOrder: number;
}
export interface OrderFormData {
orderTypeCode?: string;
categoryCode?: string;
clientId?: number;
clientName?: string;
clientContact?: string;
siteName?: string;
supplyAmount?: number;
taxAmount?: number;
totalAmount?: number;
discountRate?: number;
discountAmount?: number;
deliveryDate?: string;
deliveryMethodCode?: string;
receivedAt?: string;
memo?: string;
remarks?: string;
note?: string;
items?: OrderItemFormData[];
// 수정 페이지용 추가 필드
expectedShipDate?: string; // 출고예정일
deliveryRequestDate?: string; // 납품요청일
deliveryMethod?: string; // 배송방식 (deliveryMethodCode alias)
shippingCost?: string; // 운임비용
receiver?: string; // 수신자
receiverContact?: string; // 수신처 연락처
address?: string; // 수신처 주소
addressDetail?: string; // 상세주소
}
export interface OrderItemFormData {
itemId?: number;
itemCode?: string; // 품목코드
itemName: string;
specification?: string;
quantity: number;
unit?: string;
unitPrice: number;
}
export interface OrderStats {
total: number;
draft: number;
confirmed: number;
inProgress: number;
completed: number;
cancelled: number;
totalAmount: number;
confirmedAmount: number;
}
// 견적→수주 변환용
export interface CreateFromQuoteData {
deliveryDate?: string;
memo?: string;
}
// 생산지시 생성용
export interface CreateProductionOrderData {
processType?: 'screen' | 'slat' | 'bending';
priority?: 'urgent' | 'high' | 'normal' | 'low';
assigneeId?: number;
teamId?: number;
scheduledDate?: string;
memo?: string;
}
// 생산지시(작업지시) 타입
export interface WorkOrder {
id: string;
workOrderNo: string;
salesOrderId: number;
projectName: string | null;
processType: string;
status: string;
assigneeId?: number;
assigneeName?: string;
teamId?: number;
teamName?: string;
scheduledDate?: string;
memo?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// 생산지시 생성 결과
export interface ProductionOrderResult {
workOrder: WorkOrder;
order: Order;
}
// 견적 선택용 타입 (QuotationSelectDialog용)
export interface QuotationForSelect {
id: string;
quoteNumber: string; // KD-PR-XXXXXX-XX
grade: string; // A(우량), B(관리), C(주의)
clientId: string | null; // 발주처 ID
client: string; // 발주처명
siteName: string; // 현장명
amount: number; // 총 금액
itemCount: number; // 품목 수
registrationDate: string; // 견적일
manager?: string; // 담당자
contact?: string; // 연락처
items?: QuotationItem[]; // 품목 내역
}
export interface QuotationItem {
id: string;
itemCode: string;
itemName: string;
type: string; // 종
symbol: string; // 부호
spec: string; // 규격
quantity: number;
unit: string;
unitPrice: number;
amount: number;
}
// ============================================================================
// 상태 매핑
// ============================================================================
const API_TO_FRONTEND_STATUS: Record<string, OrderStatus> = {
'DRAFT': 'order_registered',
'CONFIRMED': 'order_confirmed',
'IN_PROGRESS': 'production_ordered',
'COMPLETED': 'shipped',
'CANCELLED': 'cancelled',
};
const FRONTEND_TO_API_STATUS: Record<OrderStatus, string> = {
'order_registered': 'DRAFT',
'order_confirmed': 'CONFIRMED',
'production_ordered': 'IN_PROGRESS',
'in_production': 'IN_PROGRESS',
'rework': 'IN_PROGRESS',
'work_completed': 'IN_PROGRESS',
'shipped': 'COMPLETED',
'cancelled': 'CANCELLED',
};
// ============================================================================
// 데이터 변환 함수
// ============================================================================
function transformApiToFrontend(apiData: ApiOrder): Order {
return {
id: String(apiData.id),
lotNumber: apiData.order_no,
quoteNumber: apiData.quote?.quote_number || '',
quoteId: apiData.quote_id ?? undefined,
orderDate: apiData.received_at || apiData.created_at.split('T')[0],
client: apiData.client_name || apiData.client?.name || '',
clientId: apiData.client_id ?? undefined,
siteName: apiData.site_name || '',
status: API_TO_FRONTEND_STATUS[apiData.status_code] || 'order_registered',
statusCode: apiData.status_code,
expectedShipDate: apiData.delivery_date ?? undefined,
deliveryMethod: apiData.delivery_method_code ?? undefined,
amount: apiData.total_amount,
supplyAmount: apiData.supply_amount,
taxAmount: apiData.tax_amount,
itemCount: apiData.items?.length || 0,
hasReceivable: false, // 추후 구현
memo: apiData.memo ?? undefined,
remarks: apiData.remarks ?? undefined,
note: apiData.note ?? undefined,
items: apiData.items?.map(transformItemApiToFrontend) || [], // 상세 페이지용 추가 필드 (API에서 매핑)
manager: apiData.client?.representative ?? undefined,
contact: apiData.client_contact ?? apiData.client?.phone ?? undefined,
deliveryRequestDate: apiData.delivery_date ?? undefined, // delivery_date를 공유
shippingCost: undefined, // API에 해당 필드 없음 - 추후 구현
receiver: undefined, // API에 해당 필드 없음 - 추후 구현
receiverContact: undefined, // API에 해당 필드 없음 - 추후 구현
address: undefined, // API에 해당 필드 없음 - 추후 구현
addressDetail: undefined, // API에 해당 필드 없음 - 추후 구현
subtotal: apiData.supply_amount,
discountRate: apiData.discount_rate,
totalAmount: apiData.total_amount,
};
}
function transformItemApiToFrontend(apiItem: ApiOrderItem): OrderItem {
return {
id: String(apiItem.id),
itemId: apiItem.item_id ?? undefined,
itemCode: apiItem.item_id ? `ITEM-${apiItem.item_id}` : undefined, // 임시: 실제 item_code는 API에서 제공 필요
itemName: apiItem.item_name,
specification: apiItem.specification ?? undefined,
spec: apiItem.specification ?? undefined, // specification alias
type: undefined, // 층 - API에 해당 필드 없음
symbol: undefined, // 부호 - API에 해당 필드 없음
quantity: apiItem.quantity,
unit: apiItem.unit ?? undefined,
unitPrice: apiItem.unit_price,
supplyAmount: apiItem.supply_amount,
taxAmount: apiItem.tax_amount,
totalAmount: apiItem.total_amount,
amount: apiItem.total_amount, // totalAmount alias
sortOrder: apiItem.sort_order,
};
}
function transformFrontendToApi(data: OrderFormData | Record<string, unknown>): Record<string, unknown> {
// Handle both API OrderFormData and Registration form's OrderFormData
const formData = data as Record<string, unknown>;
// Get client_id - handle both string (form) and number (api) types
const clientId = formData.clientId;
const clientIdValue = clientId ? (typeof clientId === 'string' ? parseInt(clientId, 10) || null : clientId) : null;
// Get items - handle both form's OrderItem[] and API's OrderItemFormData[]
const items = (formData.items as Array<Record<string, unknown>>) || [];
// Get quote_id from selectedQuotation (견적에서 수주 생성 시)
const selectedQuotation = formData.selectedQuotation as { id?: string } | undefined;
const quoteIdValue = selectedQuotation?.id ? parseInt(selectedQuotation.id, 10) || null : null;
return {
quote_id: quoteIdValue,
order_type_code: formData.orderTypeCode || 'ORDER',
category_code: formData.categoryCode || null,
client_id: clientIdValue,
client_name: formData.clientName || null,
client_contact: formData.clientContact || formData.contact || null,
site_name: formData.siteName || null,
supply_amount: formData.supplyAmount || formData.subtotal || 0,
tax_amount: formData.taxAmount || 0,
total_amount: formData.totalAmount || 0,
discount_rate: formData.discountRate || 0,
discount_amount: formData.discountAmount || 0,
delivery_date: formData.deliveryDate || formData.deliveryRequestDate || null,
delivery_method_code: formData.deliveryMethodCode || formData.deliveryMethod || null,
received_at: formData.receivedAt || null,
memo: formData.memo || null,
remarks: formData.remarks || null,
note: formData.note || null,
items: items.map((item) => {
// Handle both form's OrderItem (id, spec) and API's OrderItemFormData (itemId, specification)
// 중요: 문자열로 전달될 수 있으므로 반드시 Number()로 변환
const quantity = Number(item.quantity) || 0;
const unitPrice = Number(item.unitPrice) || 0;
const supplyAmount = quantity * unitPrice;
const taxAmount = Math.round(supplyAmount * 0.1);
return {
item_id: item.itemId || null,
item_code: item.itemCode || null,
item_name: item.itemName,
specification: item.specification || item.spec || null,
quantity,
unit: item.unit || 'EA',
unit_price: unitPrice,
supply_amount: supplyAmount,
tax_amount: taxAmount,
total_amount: supplyAmount + taxAmount,
};
}),
};
}
function transformWorkOrderApiToFrontend(apiData: ApiWorkOrder): WorkOrder {
return {
id: String(apiData.id),
workOrderNo: apiData.work_order_no,
salesOrderId: apiData.sales_order_id,
projectName: apiData.project_name,
processType: apiData.process_type,
status: apiData.status,
assigneeId: apiData.assignee_id ?? undefined,
assigneeName: apiData.assignee?.name ?? undefined,
teamId: apiData.team_id ?? undefined,
teamName: apiData.team?.name ?? undefined,
scheduledDate: apiData.scheduled_date ?? undefined,
memo: apiData.memo ?? undefined,
isActive: apiData.is_active,
createdAt: apiData.created_at,
updatedAt: apiData.updated_at,
};
}
function transformQuoteForSelect(apiData: ApiQuoteForSelect): QuotationForSelect {
return {
id: String(apiData.id),
quoteNumber: apiData.quote_number,
grade: apiData.client?.grade || 'B', // 기본값 B(관리)
clientId: apiData.client_id ? String(apiData.client_id) : null,
client: apiData.client_name || apiData.client?.name || '',
siteName: apiData.site_name || '',
amount: apiData.total_amount,
itemCount: apiData.item_count || apiData.items?.length || 0,
registrationDate: apiData.registration_date,
manager: apiData.manager ?? undefined,
contact: apiData.contact ?? apiData.client?.phone ?? undefined,
items: apiData.items?.map(transformQuoteItemForSelect),
};
}
function transformQuoteItemForSelect(apiItem: ApiQuoteItem): QuotationItem {
// QuoteItem 모델 필드명: calculated_quantity, total_price
// 수주 품목 필드명: quantity, total_amount (fallback)
// 중요: API에서 문자열로 반환될 수 있으므로 반드시 Number()로 변환
const quantity = Number(apiItem.calculated_quantity ?? apiItem.quantity ?? 0);
const unitPrice = Number(apiItem.unit_price ?? 0);
// amount fallback: total_price → total_amount → 수량 * 단가 계산
const amount = Number(apiItem.total_price ?? apiItem.total_amount ?? 0) || (quantity * unitPrice);
return {
id: String(apiItem.id),
itemCode: apiItem.item_code || '',
itemName: apiItem.item_name,
type: apiItem.type_code || '',
symbol: apiItem.symbol || '',
spec: apiItem.specification || '',
quantity,
unit: apiItem.unit || 'EA',
unitPrice,
amount,
};
}
// ============================================================================
// API 함수
// ============================================================================
/**
* 수주 목록 조회
*/
export async function getOrders(params?: {
page?: number;
size?: number;
q?: string;
status?: string;
order_type?: string;
client_id?: number;
date_from?: string;
date_to?: string;
}): Promise<{
success: boolean;
data?: { items: Order[]; total: number; page: number; totalPages: number };
error?: string;
__authError?: boolean;
}> {
try {
const searchParams = new URLSearchParams();
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('size', String(params.size));
if (params?.q) searchParams.set('q', params.q);
if (params?.status) {
// Frontend status를 API status로 변환
const apiStatus = FRONTEND_TO_API_STATUS[params.status as OrderStatus];
if (apiStatus) searchParams.set('status', apiStatus);
}
if (params?.order_type) searchParams.set('order_type', params.order_type);
if (params?.client_id) searchParams.set('client_id', String(params.client_id));
if (params?.date_from) searchParams.set('date_from', params.date_from);
if (params?.date_to) searchParams.set('date_to', params.date_to);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders?${searchParams.toString()}`,
{ method: 'GET', cache: 'no-store' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '목록 조회에 실패했습니다.' };
}
const result: ApiResponse<PaginatedResponse<ApiOrder>> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '목록 조회에 실패했습니다.' };
}
return {
success: true,
data: {
items: result.data.data.map(transformApiToFrontend),
total: result.data.total,
page: result.data.current_page,
totalPages: result.data.last_page,
},
};
} catch (error) {
console.error('[getOrders] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
/**
* 수주 상세 조회
*/
export async function getOrderById(id: string): Promise<{
success: boolean;
data?: Order;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`,
{ method: 'GET', cache: 'no-store' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '조회에 실패했습니다.' };
}
const result: ApiResponse<ApiOrder> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '조회에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
} catch (error) {
console.error('[getOrderById] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
/**
* 수주 생성
*/
export async function createOrder(data: OrderFormData): Promise<{
success: boolean;
data?: Order;
error?: string;
__authError?: boolean;
}> {
try {
const apiData = transformFrontendToApi(data);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders`,
{ method: 'POST', body: JSON.stringify(apiData) }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '등록에 실패했습니다.' };
}
const result: ApiResponse<ApiOrder> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '등록에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
} catch (error) {
console.error('[createOrder] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
/**
* 수주 수정
*/
export async function updateOrder(id: string, data: OrderFormData): Promise<{
success: boolean;
data?: Order;
error?: string;
__authError?: boolean;
}> {
try {
const apiData = transformFrontendToApi(data);
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`,
{ method: 'PUT', body: JSON.stringify(apiData) }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '수정에 실패했습니다.' };
}
const result: ApiResponse<ApiOrder> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '수정에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
} catch (error) {
console.error('[updateOrder] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
/**
* 수주 삭제
*/
export async function deleteOrder(id: string): Promise<{
success: boolean;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}`,
{ method: 'DELETE' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '삭제에 실패했습니다.' };
}
const result: ApiResponse<string> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '삭제에 실패했습니다.' };
}
return { success: true };
} catch (error) {
console.error('[deleteOrder] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
/**
* 수주 상태 변경
*/
export async function updateOrderStatus(id: string, status: OrderStatus): Promise<{
success: boolean;
data?: Order;
error?: string;
__authError?: boolean;
}> {
try {
const apiStatus = FRONTEND_TO_API_STATUS[status];
if (!apiStatus) {
return { success: false, error: '유효하지 않은 상태입니다.' };
}
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${id}/status`,
{ method: 'PATCH', body: JSON.stringify({ status: apiStatus }) }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '상태 변경에 실패했습니다.' };
}
const result: ApiResponse<ApiOrder> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '상태 변경에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
} catch (error) {
console.error('[updateOrderStatus] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
/**
* 수주 통계 조회
*/
export async function getOrderStats(): Promise<{
success: boolean;
data?: OrderStats;
error?: string;
__authError?: boolean;
}> {
try {
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/stats`,
{ method: 'GET', cache: 'no-store' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '통계 조회에 실패했습니다.' };
}
const result: ApiResponse<ApiOrderStats> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '통계 조회에 실패했습니다.' };
}
return {
success: true,
data: {
total: result.data.total,
draft: result.data.draft,
confirmed: result.data.confirmed,
inProgress: result.data.in_progress,
completed: result.data.completed,
cancelled: result.data.cancelled,
totalAmount: result.data.total_amount,
confirmedAmount: result.data.confirmed_amount,
},
};
} catch (error) {
console.error('[getOrderStats] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
/**
* 수주 일괄 삭제
*/
export async function deleteOrders(ids: string[]): Promise<{
success: boolean;
deletedCount?: number;
error?: string;
__authError?: boolean;
}> {
try {
// 순차적으로 삭제 (API에 bulk delete가 없으므로)
let deletedCount = 0;
const errors: string[] = [];
for (const id of ids) {
const result = await deleteOrder(id);
if (result.success) {
deletedCount++;
} else {
errors.push(result.error || `ID ${id} 삭제 실패`);
}
}
if (deletedCount === 0 && errors.length > 0) {
return { success: false, error: errors[0] };
}
return { success: true, deletedCount };
} catch (error) {
console.error('[deleteOrders] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
/**
* 견적에서 수주 생성
*/
export async function createOrderFromQuote(
quoteId: number,
data?: CreateFromQuoteData
): Promise<{
success: boolean;
data?: Order;
error?: string;
__authError?: boolean;
}> {
try {
const apiData: Record<string, unknown> = {};
if (data?.deliveryDate) apiData.delivery_date = data.deliveryDate;
if (data?.memo) apiData.memo = data.memo;
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/from-quote/${quoteId}`,
{ method: 'POST', body: JSON.stringify(apiData) }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '수주 생성에 실패했습니다.' };
}
const result: ApiResponse<ApiOrder> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '수주 생성에 실패했습니다.' };
}
return { success: true, data: transformApiToFrontend(result.data) };
} catch (error) {
console.error('[createOrderFromQuote] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
/**
* 생산지시 생성
*/
export async function createProductionOrder(
orderId: string,
data?: CreateProductionOrderData
): Promise<{
success: boolean;
data?: ProductionOrderResult;
error?: string;
__authError?: boolean;
}> {
try {
const apiData: Record<string, unknown> = {};
if (data?.processType) apiData.process_type = data.processType;
if (data?.priority) apiData.priority = data.priority;
if (data?.assigneeId) apiData.assignee_id = data.assigneeId;
if (data?.teamId) apiData.team_id = data.teamId;
if (data?.scheduledDate) apiData.scheduled_date = data.scheduledDate;
if (data?.memo) apiData.memo = data.memo;
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/orders/${orderId}/production-order`,
{ method: 'POST', body: JSON.stringify(apiData) }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '생산지시 생성에 실패했습니다.' };
}
const result: ApiResponse<ApiProductionOrderResponse> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '생산지시 생성에 실패했습니다.' };
}
return {
success: true,
data: {
workOrder: transformWorkOrderApiToFrontend(result.data.work_order),
order: transformApiToFrontend(result.data.order),
},
};
} catch (error) {
console.error('[createProductionOrder] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}
/**
* 수주 변환용 확정 견적 목록 조회
* QuotationSelectDialog에서 사용
*/
export async function getQuotesForSelect(params?: {
q?: string;
page?: number;
size?: number;
}): Promise<{
success: boolean;
data?: { items: QuotationForSelect[]; total: number };
error?: string;
__authError?: boolean;
}> {
try {
const searchParams = new URLSearchParams();
// 확정(finalized) 상태의 견적만 조회
searchParams.set('status', 'finalized');
// 품목 포함 (수주 전환용)
searchParams.set('with_items', 'true');
if (params?.q) searchParams.set('q', params.q);
if (params?.page) searchParams.set('page', String(params.page));
if (params?.size) searchParams.set('size', String(params.size || 50));
const { response, error } = await serverFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes?${searchParams.toString()}`,
{ method: 'GET', cache: 'no-store' }
);
if (error) {
return { success: false, error: error.message, __authError: error.code === 'UNAUTHORIZED' };
}
if (!response) {
return { success: false, error: '견적 목록 조회에 실패했습니다.' };
}
const result: ApiResponse<PaginatedResponse<ApiQuoteForSelect>> = await response.json();
if (!response.ok || !result.success) {
return { success: false, error: result.message || '견적 목록 조회에 실패했습니다.' };
}
return {
success: true,
data: {
items: result.data.data.map(transformQuoteForSelect),
total: result.data.total,
},
};
} catch (error) {
console.error('[getQuotesForSelect] Error:', error);
return { success: false, error: '서버 오류가 발생했습니다.' };
}
}