- 전환 가능한 견적 {filteredQuotations.length}건 (최종확정 상태)
+ {isLoading ? (
+
+
+ 견적 목록을 불러오는 중...
+
+ ) : error ? (
+ {error}
+ ) : (
+ `전환 가능한 견적 ${quotations.length}건 (최종확정 상태)`
+ )}
{/* 견적 목록 */}
- {filteredQuotations.map((quotation) => (
-
handleSelect(quotation)}
- className={cn(
- "p-4 border rounded-lg cursor-pointer transition-colors",
- "hover:bg-muted/50 hover:border-primary/50",
- selectedId === quotation.id && "border-primary bg-primary/5"
- )}
- >
- {/* 상단: 견적번호 + 등급 */}
-
-
-
- {quotation.quoteNumber}
-
-
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+ {quotations.map((quotation) => (
+
handleSelect(quotation)}
+ className={cn(
+ "p-4 border rounded-lg cursor-pointer transition-colors",
+ "hover:bg-muted/50 hover:border-primary/50",
+ selectedId === quotation.id && "border-primary bg-primary/5"
+ )}
+ >
+ {/* 상단: 견적번호 + 등급 */}
+
+
+
+ {quotation.quoteNumber}
+
+
+
+ {selectedId === quotation.id && (
+
+ )}
+
+
+ {/* 발주처 */}
+
+ {quotation.client}
+
+
+ {/* 현장명 + 금액 */}
+
+
+ [{quotation.siteName}]
+
+
+ {formatAmount(quotation.amount)}원
+
+
+
+ {/* 품목 수 */}
+
+ {quotation.itemCount}개 품목
+
- {selectedId === quotation.id && (
-
- )}
-
+ ))}
- {/* 발주처 */}
-
- {quotation.client}
-
-
- {/* 현장명 + 금액 */}
-
-
- [{quotation.siteName}]
-
-
- {formatAmount(quotation.amount)}원
-
-
-
- {/* 품목 수 */}
-
- {quotation.itemCount}개 품목
-
-
- ))}
-
- {filteredQuotations.length === 0 && (
-
- 검색 결과가 없습니다.
-
+ {quotations.length === 0 && !error && (
+
+ 검색 결과가 없습니다.
+
+ )}
+ >
)}
diff --git a/src/components/orders/actions.ts b/src/components/orders/actions.ts
index 2f81de08..6a5dd22a 100644
--- a/src/components/orders/actions.ts
+++ b/src/components/orders/actions.ts
@@ -70,6 +70,47 @@ interface ApiQuote {
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;
+ grade?: string;
+ representative?: string;
+ phone?: string;
+ } | null;
+ items?: ApiQuoteItem[];
+}
+
+interface ApiQuoteItem {
+ id: number;
+ item_code?: string;
+ item_name: string;
+ type_code?: string;
+ symbol?: string;
+ specification?: string;
+ quantity: number;
+ unit?: string;
+ unit_price: number;
+ supply_amount: number;
+ tax_amount: number;
+ total_amount: number;
+}
+
interface ApiWorkOrder {
id: number;
tenant_id: number;
@@ -249,6 +290,7 @@ export interface CreateFromQuoteData {
// 생산지시 생성용
export interface CreateProductionOrderData {
processType?: 'screen' | 'slat' | 'bending';
+ priority?: 'urgent' | 'high' | 'normal' | 'low';
assigneeId?: number;
teamId?: number;
scheduledDate?: string;
@@ -280,6 +322,34 @@ export interface ProductionOrderResult {
order: Order;
}
+// 견적 선택용 타입 (QuotationSelectDialog용)
+export interface QuotationForSelect {
+ id: string;
+ quoteNumber: string; // KD-PR-XXXXXX-XX
+ grade: string; // A(우량), B(관리), C(주의)
+ 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;
+}
+
// ============================================================================
// 상태 매핑
// ============================================================================
@@ -415,6 +485,37 @@ function transformWorkOrderApiToFrontend(apiData: ApiWorkOrder): WorkOrder {
};
}
+function transformQuoteForSelect(apiData: ApiQuoteForSelect): QuotationForSelect {
+ return {
+ id: String(apiData.id),
+ quoteNumber: apiData.quote_number,
+ grade: apiData.client?.grade || 'B', // 기본값 B(관리)
+ 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 {
+ return {
+ id: String(apiItem.id),
+ itemCode: apiItem.item_code || '',
+ itemName: apiItem.item_name,
+ type: apiItem.type_code || '',
+ symbol: apiItem.symbol || '',
+ spec: apiItem.specification || '',
+ quantity: apiItem.quantity,
+ unit: apiItem.unit || 'EA',
+ unitPrice: apiItem.unit_price,
+ amount: apiItem.total_amount,
+ };
+}
+
// ============================================================================
// API 함수
// ============================================================================
@@ -815,6 +916,7 @@ export async function createProductionOrder(
try {
const apiData: Record
= {};
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;
@@ -851,3 +953,58 @@ export async function createProductionOrder(
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');
+ 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> = 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: '서버 오류가 발생했습니다.' };
+ }
+}