'use server'; import { executeServerAction } from '@/lib/api/execute-server-action'; import { buildApiUrl } from '@/lib/api/query-params'; import type { PaginatedApiResponse } from '@/lib/api/types'; import { formatDate } from '@/lib/utils/date'; // ============================================================================ // 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; root_nodes_sum_quantity: number | null; supply_amount: number; tax_amount: number; total_amount: number; discount_rate: number; discount_amount: number; delivery_date: string | null; delivery_method_code: string | null; delivery_method_label?: string; // API에서 조회한 배송방식 라벨 shipping_cost_label?: string; // API에서 조회한 운임비용 라벨 received_at: string | null; memo: string | null; remarks: string | null; note: string | null; options: { shipping_cost_code?: string; receiver?: string; receiver_contact?: string; shipping_address?: string; shipping_address_detail?: string; manager_name?: string; } | null; created_by: number | null; updated_by: number | null; created_at: string; updated_at: string; client?: ApiClient | null; items?: ApiOrderItem[]; root_nodes?: ApiOrderNode[]; quote?: ApiQuote | null; } interface ApiOrderItem { id: number; order_id: number; order_node_id: number | null; item_id: number | null; item_code: string | null; item_name: string; specification: string | null; // 제품-부품 매핑용 코드 floor_code: string | null; symbol_code: string | null; quantity: number; unit: string | null; unit_price: number; supply_amount: number; tax_amount: number; total_amount: number; sort_order: number; } interface ApiOrderNode { id: number; order_id: number; parent_id: number | null; node_type: string; code: string; name: string; status_code: string; quantity: number; unit_price: number; total_price: number; options: Record | null; depth: number; sort_order: number; children?: ApiOrderNode[]; items?: ApiOrderItem[]; } interface ApiClient { id: number; name: string; business_no?: string; contact_person?: string; manager_name?: string; phone?: string; email?: string; grade?: string; } interface ApiQuote { id: number; quote_no: string; quote_number?: string; site_name: string | null; calculation_inputs?: { items?: Array<{ productCategory?: string; productName?: string; openWidth?: string; openHeight?: string; quantity?: number; floor?: string; code?: 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; grade?: string; // 등급 (A, B, C) } | null; items?: ApiQuoteItem[]; calculation_inputs?: { items?: Array<{ productCategory?: string; productCode?: string; productName?: string; openWidth?: string; openHeight?: string; quantity?: number; floor?: string; code?: string; }>; } | null; } interface ApiQuoteItem { id: number; item_id?: number | null; // Items Master 참조 ID item_code?: string; item_name: string; type_code?: string; symbol?: string; note?: string; // "5F FSS-01" 형태 (floor + code) 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_id: number | 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; process?: { id: number; process_name: string } | null; } interface ApiProductionOrderResponse { work_order?: ApiWorkOrder; work_orders?: 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 { success: boolean; message: string; data: T; } // ============================================================================ // Frontend 타입 정의 // ============================================================================ // 수주 상태 타입 (API와 매핑) export type OrderStatus = | 'order_registered' // DRAFT | 'order_confirmed' // CONFIRMED | 'production_ordered' // IN_PROGRESS | 'in_production' // IN_PRODUCTION | 'produced' // PRODUCED | 'shipping' // SHIPPING | 'shipped' // SHIPPED | 'completed' // COMPLETED | 'rework' // (세부 - 레거시) | 'work_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 deliveryMethodLabel?: string; // 배송방식 라벨 (API에서 조회) amount: number; // total_amount supplyAmount: number; taxAmount: number; itemCount: number; // items.length hasReceivable?: boolean; // 미수 여부 (추후 구현) memo?: string; remarks?: string; note?: string; items?: OrderItem[]; nodes?: OrderNode[]; // 목록 페이지용 추가 필드 productName?: string; // 제품명 (첫 번째 품목명) receiverAddress?: string; // 수신주소 receiverPlace?: string; // 수신처 (전화번호) frameCount?: number; // 틀수 (수량) // 상세 페이지용 추가 필드 manager?: string; // 담당자 contact?: string; // 연락처 (client_contact) deliveryRequestDate?: string; // 납품요청일 shippingCost?: string; // 운임비용 (코드) shippingCostLabel?: string; // 운임비용 (라벨) receiver?: string; // 수신자 receiverContact?: string; // 수신처 연락처 address?: string; // 수신처 주소 addressDetail?: string; // 상세주소 subtotal?: number; // 소계 (supply_amount와 동일) discountRate?: number; // 할인율 totalAmount?: number; // 총금액 (amount와 동일하지만 명시적) // 제품 정보 (견적의 calculation_inputs에서 가져옴) products?: Array<{ productName: string; productCategory?: string; openWidth?: string; openHeight?: string; quantity: number; floor?: string; code?: string; }>; } export interface OrderNode { id: number; parentId: number | null; nodeType: string; code: string; name: string; statusCode: string; quantity: number; unitPrice: number; totalPrice: number; options: Record | null; depth: number; sortOrder: number; children: OrderNode[]; items: OrderItem[]; } 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; width?: number; // 가로 (mm) - ItemAddDialog 호환 height?: number; // 세로 (mm) - ItemAddDialog 호환 } 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; // 대시보드 통계용 추가 필드 (API에서 선택적으로 반환) thisMonthAmount?: number; splitPending?: number; productionPending?: number; shipPending?: number; } // 견적→수주 변환용 export interface CreateFromQuoteData { deliveryDate?: string; memo?: string; } // 생산지시 생성용 export interface CreateProductionOrderData { processId?: number; processIds?: number[]; // 공정별 다중 작업지시 생성용 priority?: 'urgent' | 'high' | 'normal' | 'low'; assigneeId?: number; assigneeIds?: number[]; // 다중 담당자 선택용 teamId?: number; departmentId?: number; // 부서 ID scheduledDate?: string; memo?: string; } // 생산지시(작업지시) 타입 export interface WorkOrder { id: string; workOrderNo: string; salesOrderId: number; projectName: string | null; processId?: number; processType: string; status: string; assigneeId?: number; assigneeName?: string; teamId?: number; teamName?: string; scheduledDate?: string; memo?: string; isActive: boolean; createdAt: string; updatedAt: string; process?: { id: number; processName: string }; } // 생산지시 생성 결과 export interface ProductionOrderResult { workOrder?: WorkOrder; workOrders?: 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[]; // 품목 내역 calculationInputs?: { items?: Array<{ productCategory?: string; productCode?: string; productName?: string; openWidth?: string; openHeight?: string; quantity?: number; floor?: string; code?: string; }>; }; } export interface QuotationItem { id: string; itemId?: string | null; // Items Master 참조 ID itemCode: string; itemName: string; type: string; // 종 symbol: string; // 부호 spec: string; // 규격 quantity: number; unit: string; unitPrice: number; amount: number; } // ============================================================================ // 상태 매핑 // ============================================================================ const API_TO_FRONTEND_STATUS: Record = { 'DRAFT': 'order_registered', 'CONFIRMED': 'order_confirmed', 'IN_PROGRESS': 'production_ordered', 'IN_PRODUCTION': 'in_production', 'PRODUCED': 'produced', 'SHIPPING': 'shipping', 'SHIPPED': 'shipped', 'COMPLETED': 'completed', 'CANCELLED': 'cancelled', }; const FRONTEND_TO_API_STATUS: Record = { 'order_registered': 'DRAFT', 'order_confirmed': 'CONFIRMED', 'production_ordered': 'IN_PROGRESS', 'in_production': 'IN_PRODUCTION', 'produced': 'PRODUCED', 'shipping': 'SHIPPING', 'shipped': 'SHIPPED', 'completed': 'COMPLETED', 'rework': 'IN_PROGRESS', 'work_completed': 'IN_PROGRESS', '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 || formatDate(apiData.created_at), 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, deliveryMethodLabel: apiData.delivery_method_label ?? 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) || [], nodes: apiData.root_nodes?.map(transformNodeApiToFrontend) || [], // 목록 페이지용 추가 필드: 첫 root_node의 options.product_name (FG 제품명) productName: (apiData.root_nodes?.[0]?.options?.product_name as string) || undefined, receiverAddress: apiData.options?.shipping_address ?? undefined, receiverPlace: apiData.options?.receiver_contact ?? undefined, frameCount: apiData.root_nodes_sum_quantity ?? apiData.quantity ?? undefined, // 상세 페이지용 추가 필드 (API에서 매핑) manager: apiData.options?.manager_name ?? apiData.client?.manager_name ?? undefined, contact: apiData.client_contact ?? apiData.client?.phone ?? undefined, deliveryRequestDate: apiData.delivery_date ?? undefined, // delivery_date를 공유 // options JSON에서 추출 shippingCost: apiData.options?.shipping_cost_code ?? undefined, shippingCostLabel: apiData.shipping_cost_label ?? undefined, receiver: apiData.options?.receiver ?? undefined, receiverContact: apiData.options?.receiver_contact ?? undefined, address: apiData.options?.shipping_address ?? undefined, addressDetail: apiData.options?.shipping_address_detail ?? undefined, subtotal: apiData.supply_amount, discountRate: apiData.discount_rate, totalAmount: apiData.total_amount, // 제품 정보 (견적의 calculation_inputs에서 추출) products: apiData.quote?.calculation_inputs?.items?.map(item => ({ productName: item.productName || '', productCategory: item.productCategory, openWidth: item.openWidth, openHeight: item.openHeight, quantity: item.quantity || 1, floor: item.floor, code: item.code, })) || [], }; } function transformNodeApiToFrontend(apiNode: ApiOrderNode): OrderNode { return { id: apiNode.id, parentId: apiNode.parent_id, nodeType: apiNode.node_type, code: apiNode.code, name: apiNode.name, statusCode: apiNode.status_code, quantity: apiNode.quantity, unitPrice: apiNode.unit_price, totalPrice: apiNode.total_price, options: apiNode.options, depth: apiNode.depth, sortOrder: apiNode.sort_order, children: (apiNode.children || []).map(transformNodeApiToFrontend), items: (apiNode.items || []).map(transformItemApiToFrontend), }; } function transformItemApiToFrontend(apiItem: ApiOrderItem): OrderItem { return { id: String(apiItem.id), itemId: apiItem.item_id ?? undefined, itemCode: apiItem.item_code ?? undefined, itemName: apiItem.item_name, specification: apiItem.specification ?? undefined, spec: apiItem.specification ?? undefined, // specification alias type: apiItem.floor_code ?? undefined, // 층 코드 (제품-부품 매핑용) symbol: apiItem.symbol_code ?? undefined, // 부호 코드 (제품-부품 매핑용) 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): Record { // Handle both API OrderFormData and Registration form's OrderFormData const formData = data as Record; // 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>) || []; // Get quote_id from selectedQuotation (견적에서 수주 생성 시) const selectedQuotation = formData.selectedQuotation as { id?: string } | undefined; const quoteIdValue = selectedQuotation?.id ? parseInt(selectedQuotation.id, 10) || null : null; // Build result object - only include client_id if explicitly provided // to avoid overwriting existing value with null on update const result: Record = { quote_id: quoteIdValue, order_type_code: formData.orderTypeCode || 'ORDER', category_code: formData.categoryCode || null, // client_id is conditionally added below 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, // options JSON으로 묶어서 저장 (운임비용, 수신자, 수신처 연락처, 주소) options: { shipping_cost_code: formData.shippingCost || null, receiver: formData.receiver || null, receiver_contact: formData.receiverContact || null, shipping_address: formData.address || null, shipping_address_detail: formData.addressDetail || null, manager_name: formData.manager || 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, floor_code: item.type || null, symbol_code: item.symbol || null, }; }), }; // Only include client_id if explicitly provided (not undefined) // This prevents overwriting existing client_id with null on update if (clientId !== undefined) { result.client_id = clientIdValue; } return result; } 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, processId: apiData.process_id ?? undefined, process: apiData.process ? { id: apiData.process.id, processName: apiData.process.process_name, } : undefined, }; } 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), calculationInputs: apiData.calculation_inputs ? { items: apiData.calculation_inputs.items?.map(item => ({ productCategory: item.productCategory, productCode: item.productCode, productName: item.productName, openWidth: item.openWidth, openHeight: item.openHeight, quantity: item.quantity, floor: item.floor, code: item.code, })), } : undefined, }; } 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); // note에서 floor+code 추출: "5F FSS-01" → type="5F", symbol="FSS-01" let typeFromNote = apiItem.type_code || ''; let symbolFromNote = apiItem.symbol || ''; if (!typeFromNote && !symbolFromNote && apiItem.note) { const noteParts = apiItem.note.trim().split(/\s+/); if (noteParts.length >= 2) { typeFromNote = noteParts[0]; symbolFromNote = noteParts.slice(1).join(' '); } } return { id: String(apiItem.id), itemId: apiItem.item_id ? String(apiItem.item_id) : null, itemCode: apiItem.item_code || '', itemName: apiItem.item_name, type: typeFromNote, symbol: symbolFromNote, 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; }> { const apiStatus = params?.status ? FRONTEND_TO_API_STATUS[params.status as OrderStatus] : undefined; const result = await executeServerAction>({ url: buildApiUrl('/api/v1/orders', { page: params?.page, size: params?.size, q: params?.q, status: apiStatus, order_type: params?.order_type, client_id: params?.client_id, date_from: params?.date_from, date_to: params?.date_to, }), errorMessage: '목록 조회에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, data: { items: result.data.data.map(transformApiToFrontend), total: result.data.total, page: result.data.current_page, totalPages: result.data.last_page, }, }; } /** * 수주 상세 조회 */ export async function getOrderById(id: string): Promise<{ success: boolean; data?: Order; error?: string; __authError?: boolean; }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/orders/${id}`), transform: (data: ApiOrder) => transformApiToFrontend(data), errorMessage: '조회에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } /** * 수주 생성 */ export async function createOrder(data: OrderFormData | Record): Promise<{ success: boolean; data?: Order; error?: string; __authError?: boolean; }> { const apiData = transformFrontendToApi(data); const result = await executeServerAction({ url: buildApiUrl('/api/v1/orders'), method: 'POST', body: apiData, transform: (d: ApiOrder) => transformApiToFrontend(d), errorMessage: '등록에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } /** * 수주 수정 */ export async function updateOrder(id: string, data: OrderFormData | Record): Promise<{ success: boolean; data?: Order; error?: string; __authError?: boolean; }> { const apiData = transformFrontendToApi(data); const result = await executeServerAction({ url: buildApiUrl(`/api/v1/orders/${id}`), method: 'PUT', body: apiData, transform: (d: ApiOrder) => transformApiToFrontend(d), errorMessage: '수정에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } /** * 수주 삭제 */ export async function deleteOrder(id: string): Promise<{ success: boolean; error?: string; __authError?: boolean; }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/orders/${id}`), method: 'DELETE', errorMessage: '삭제에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, error: result.error }; } /** * 수주 상태 변경 */ export async function updateOrderStatus(id: string, status: OrderStatus): Promise<{ success: boolean; data?: Order; error?: string; __authError?: boolean; }> { const apiStatus = FRONTEND_TO_API_STATUS[status]; if (!apiStatus) { return { success: false, error: '유효하지 않은 상태입니다.' }; } const result = await executeServerAction({ url: buildApiUrl(`/api/v1/orders/${id}/status`), method: 'PATCH', body: { status: apiStatus }, transform: (d: ApiOrder) => transformApiToFrontend(d), errorMessage: '상태 변경에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } /** * 수주 통계 조회 */ export async function getOrderStats(): Promise<{ success: boolean; data?: OrderStats; error?: string; __authError?: boolean; }> { const result = await executeServerAction({ url: buildApiUrl('/api/v1/orders/stats'), errorMessage: '통계 조회에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; 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, }, }; } /** * 수주 일괄 삭제 (Bulk API) */ export async function deleteOrders( ids: string[], options?: { force?: boolean } ): Promise<{ success: boolean; deletedCount?: number; skippedCount?: number; skippedIds?: number[]; error?: string; __authError?: boolean; }> { interface BulkDeleteResponse { deleted_count: number; skipped_count: number; skipped_ids: number[]; } const body: Record = { ids: ids.map(Number) }; if (options?.force) body.force = true; const result = await executeServerAction({ url: buildApiUrl('/api/v1/orders/bulk'), method: 'DELETE', body, errorMessage: '수주 일괄 삭제에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, deletedCount: result.data.deleted_count, skippedCount: result.data.skipped_count, skippedIds: result.data.skipped_ids, }; } /** * 견적에서 수주 생성 */ export async function createOrderFromQuote( quoteId: number, data?: CreateFromQuoteData ): Promise<{ success: boolean; data?: Order; error?: string; __authError?: boolean; }> { const apiData: Record = {}; if (data?.deliveryDate) apiData.delivery_date = data.deliveryDate; if (data?.memo) apiData.memo = data.memo; const result = await executeServerAction({ url: buildApiUrl(`/api/v1/orders/from-quote/${quoteId}`), method: 'POST', body: apiData, transform: (d: ApiOrder) => transformApiToFrontend(d), errorMessage: '수주 생성에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.error }; } /** * 생산지시 생성 */ export async function createProductionOrder( orderId: string, data?: CreateProductionOrderData ): Promise<{ success: boolean; data?: ProductionOrderResult; error?: string; __authError?: boolean; }> { const apiData: Record = {}; if (data?.processIds && data.processIds.length > 0) { apiData.process_ids = data.processIds; } else if (data?.processId) { apiData.process_id = data.processId; } if (data?.priority) apiData.priority = data.priority; if (data?.assigneeIds && data.assigneeIds.length > 0) { apiData.assignee_ids = data.assigneeIds; } else if (data?.assigneeId) { apiData.assignee_id = data.assigneeId; } if (data?.teamId) apiData.team_id = data.teamId; if (data?.departmentId) apiData.department_id = data.departmentId; if (data?.scheduledDate) apiData.scheduled_date = data.scheduledDate; if (data?.memo) apiData.memo = data.memo; const result = await executeServerAction({ url: buildApiUrl(`/api/v1/orders/${orderId}/production-order`), method: 'POST', body: apiData, errorMessage: '생산지시 생성에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; const responseData: ProductionOrderResult = { order: transformApiToFrontend(result.data.order), }; if (result.data.work_orders && result.data.work_orders.length > 0) { responseData.workOrders = result.data.work_orders.map(transformWorkOrderApiToFrontend); } else if (result.data.work_order) { responseData.workOrder = transformWorkOrderApiToFrontend(result.data.work_order); } return { success: true, data: responseData }; } /** * 생산지시 되돌리기 (작업지시 및 관련 데이터 삭제) */ export async function revertProductionOrder( orderId: string, options?: { force?: boolean; reason?: string } ): Promise<{ success: boolean; data?: { order: Order; deletedCounts: { workResults: number; workOrderItems: number; workOrders: number }; previousStatus: string; }; error?: string; __authError?: boolean; }> { interface RevertResponse { order: ApiOrder; deleted_counts: { work_results: number; work_order_items: number; work_orders: number }; cancelled_counts?: { work_orders: number; work_order_items: number }; previous_status: string; } const body: Record = {}; if (options?.force !== undefined) body.force = options.force; if (options?.reason) body.reason = options.reason; const result = await executeServerAction({ url: buildApiUrl(`/api/v1/orders/${orderId}/revert-production`), method: 'POST', body: Object.keys(body).length > 0 ? body : undefined, errorMessage: '생산지시 되돌리기에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, data: { order: transformApiToFrontend(result.data.order), deletedCounts: { workResults: result.data.deleted_counts.work_results, workOrderItems: result.data.deleted_counts.work_order_items, workOrders: result.data.deleted_counts.work_orders, }, previousStatus: result.data.previous_status, }, }; } /** * 수주확정 되돌리기 (수주등록 상태로 변경) */ export async function revertOrderConfirmation(orderId: string): Promise<{ success: boolean; data?: { order: Order; previousStatus: string }; error?: string; __authError?: boolean; }> { interface RevertConfirmResponse { order: ApiOrder; previous_status: string } const result = await executeServerAction({ url: buildApiUrl(`/api/v1/orders/${orderId}/revert-confirmation`), method: 'POST', errorMessage: '수주확정 되돌리기에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, data: { order: transformApiToFrontend(result.data.order), previousStatus: result.data.previous_status, }, }; } /** * 절곡 BOM 품목 재고 현황 조회 */ export interface BendingStockItem { itemId: number; itemCode: string; itemName: string; unit: string; neededQty: number; stockQty: number; reservedQty: number; availableQty: number; shortfallQty: number; status: 'sufficient' | 'insufficient'; } export async function checkBendingStock(orderId: string): Promise<{ success: boolean; data?: BendingStockItem[]; error?: string; __authError?: boolean; }> { interface ApiBendingStockItem { item_id: number; item_code: string; item_name: string; unit: string; needed_qty: number; stock_qty: number; reserved_qty: number; available_qty: number; shortfall_qty: number; status: string; } const result = await executeServerAction({ url: buildApiUrl(`/api/v1/orders/${orderId}/bending-stock`), errorMessage: '절곡 재고 현황 조회에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; const items: BendingStockItem[] = result.data.map((item) => ({ itemId: item.item_id, itemCode: item.item_code, itemName: item.item_name, unit: item.unit, neededQty: item.needed_qty, stockQty: item.stock_qty, reservedQty: item.reserved_qty, availableQty: item.available_qty, shortfallQty: item.shortfall_qty, status: item.status as 'sufficient' | 'insufficient', })); return { success: true, data: items }; } /** * 수주 변환용 단일 견적 조회 (ID로 조회) * 견적 상세페이지에서 수주등록 버튼 클릭 시 사용 */ export async function getQuoteByIdForSelect(id: string): Promise<{ success: boolean; data?: QuotationForSelect; error?: string; __authError?: boolean; }> { const result = await executeServerAction({ url: buildApiUrl(`/api/v1/quotes/${id}`, { with_items: true }), transform: (data: ApiQuoteForSelect) => transformQuoteForSelect(data), errorMessage: '견적 조회에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; return { success: result.success, data: result.data, error: result.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; }> { const result = await executeServerAction>({ url: buildApiUrl('/api/v1/quotes', { status: 'finalized', with_items: 'true', for_order: 'true', q: params?.q, page: params?.page, size: params?.size || 50, }), errorMessage: '견적 목록 조회에 실패했습니다.', }); if (result.__authError) return { success: false, __authError: true }; if (!result.success || !result.data) return { success: false, error: result.error }; return { success: true, data: { items: result.data.data.map(transformQuoteForSelect), total: result.data.total, }, }; }