diff --git a/src/components/business/construction/estimates/EstimateListClient.tsx b/src/components/business/construction/estimates/EstimateListClient.tsx index 3205e59b..ea7b1a43 100644 --- a/src/components/business/construction/estimates/EstimateListClient.tsx +++ b/src/components/business/construction/estimates/EstimateListClient.tsx @@ -104,7 +104,7 @@ export default function EstimateListClient({ initialData = [], initialStats }: E try { const [listResult, statsResult] = await Promise.all([ getEstimateList({ - size: 1000, + size: 100, // API 최대값 100 startDate: startDate || undefined, endDate: endDate || undefined, }), diff --git a/src/components/business/construction/estimates/actions.ts b/src/components/business/construction/estimates/actions.ts index 252afada..719197b1 100644 --- a/src/components/business/construction/estimates/actions.ts +++ b/src/components/business/construction/estimates/actions.ts @@ -17,47 +17,73 @@ import type { import { apiClient } from '@/lib/api'; /** - * 주일 기업 - 견적관리 Server Actions - * 표준화된 apiClient 사용 버전 + * 건설 프로젝트 - 견적관리 Server Actions + * quotes API 사용 (quote_type=construction 필터) */ // ======================================== -// API 응답 타입 +// API 응답 타입 (Quotes API) // ======================================== -interface ApiEstimate { +interface ApiQuote { id: number; - estimate_code: string; - partner_id: number | null; - partner_name: string | null; - project_name: string; - estimator_id: number | null; - estimator_name: string | null; - item_count: number; - estimate_amount: number; - completed_date: string | null; - bid_date: string | null; - status: 'pending' | 'approval_waiting' | 'completed' | 'rejected' | 'hold'; + quote_type: string; + quote_number: string; + registration_date: string; + client_id: number | null; + client_name: string | null; + site_id: number | null; + site_name: string | null; + site_briefing_id: number | null; + product_category: string | null; + product_name: string | null; + total_amount: number | string; + status: string; + author: string | null; + manager: string | null; + remarks: string | null; created_at: string; updated_at: string; - created_by: string | null; + created_by: number | null; + // 연관 데이터 + items?: ApiQuoteItem[]; + site_briefing?: ApiSiteBriefing; } -interface ApiEstimateStats { +interface ApiQuoteItem { + id: number; + item_code: string; + item_name: string; + specification: string | null; + unit: string; + base_quantity: number; + calculated_quantity: number; + unit_price: number; + total_price: number; + formula: string | null; + note: string | null; + sort_order: number; +} + +interface ApiSiteBriefing { + id: number; + briefing_code: string; + title: string; + briefing_date: string; + attendance_status: string; + partner?: { + id: number; + name: string; + }; +} + +interface ApiQuoteStats { total_count: number; pending_count: number; completed_count: number; } -interface ApiEstimateDetail extends ApiEstimate { - site_briefing?: ApiSiteBriefingInfo; - bid_info?: ApiBidInfo; - summary_items?: ApiSummaryItem[]; - expense_items?: ApiExpenseItem[]; - price_adjustments?: ApiPriceAdjustmentItem[]; - detail_items?: ApiDetailItem[]; -} - +// Legacy API types (for backward compatibility) interface ApiSiteBriefingInfo { briefing_code: string; partner_name: string; @@ -85,202 +111,116 @@ interface ApiBidDocument { file_size: number; } -interface ApiSummaryItem { - id: number; - name: string; - quantity: number; - unit: string; - material_cost: number; - labor_cost: number; - total_cost: number; - remarks: string; -} - -interface ApiExpenseItem { - id: number; - name: string; - amount: number; - selected: boolean; -} - -interface ApiPriceAdjustmentItem { - id: number; - category: string; - unit_price: number; - coating: number; - batting: number; - box_reinforce: number; - painting: number; - total: number; -} - -interface ApiDetailItem { - id: number; - no: number; - name: string; - material: string; - width: number; - height: number; - quantity: number; - box: number; - assembly: number; - coating: number; - batting: number; - mounting: number; - fitting: number; - controller: number; - width_construction: number; - height_construction: number; - material_cost: number; - labor_cost: number; - quantity_price: number; - expense_quantity: number; - expense_total: number; - total_cost: number; - other_cost: number; - margin_cost: number; - total_price: number; - unit_price: number; - expense: number; - margin_rate: number; - unit_quantity: number; - expense_result: number; - margin_actual: number; -} - // ======================================== // 타입 변환 함수 // ======================================== /** - * API 응답 → Estimate 타입 변환 + * API 응답 (Quote) → Estimate 타입 변환 + * 기존 프론트엔드 타입과 호환성 유지 */ -function transformEstimate(apiData: ApiEstimate): Estimate { +function transformQuoteToEstimate(apiData: ApiQuote): Estimate { return { id: String(apiData.id), - estimateCode: apiData.estimate_code || '', - partnerId: apiData.partner_id ? String(apiData.partner_id) : '', - partnerName: apiData.partner_name || '', - projectName: apiData.project_name || '', - estimatorId: apiData.estimator_id ? String(apiData.estimator_id) : '', - estimatorName: apiData.estimator_name || '', - itemCount: apiData.item_count || 0, - estimateAmount: apiData.estimate_amount || 0, - completedDate: apiData.completed_date || null, - bidDate: apiData.bid_date || null, - status: apiData.status || 'pending', + estimateCode: apiData.quote_number || '', + partnerId: apiData.client_id ? String(apiData.client_id) : '', + partnerName: apiData.client_name || '', + projectName: apiData.site_name || '', + estimatorId: apiData.created_by ? String(apiData.created_by) : '', + estimatorName: apiData.author || '', + itemCount: apiData.items?.length || 0, + estimateAmount: Number(apiData.total_amount) || 0, + completedDate: null, + bidDate: apiData.registration_date || null, + status: mapQuoteStatusToEstimateStatus(apiData.status), createdAt: apiData.created_at || '', updatedAt: apiData.updated_at || '', - createdBy: apiData.created_by || '', + createdBy: apiData.created_by ? String(apiData.created_by) : '', }; } +/** + * Quote 상태 → Estimate 상태 매핑 + */ +function mapQuoteStatusToEstimateStatus( + quoteStatus: string +): 'pending' | 'approval_waiting' | 'completed' | 'rejected' | 'hold' { + const statusMap: Record = { + pending: 'pending', + draft: 'pending', + sent: 'approval_waiting', + approved: 'completed', + rejected: 'rejected', + finalized: 'completed', + converted: 'completed', + }; + return statusMap[quoteStatus] || 'pending'; +} + /** * API 응답 → EstimateDetail 타입 변환 */ -function transformEstimateDetail(apiData: ApiEstimateDetail): EstimateDetail { - const base = transformEstimate(apiData); +function transformQuoteToEstimateDetail(apiData: ApiQuote): EstimateDetail { + const base = transformQuoteToEstimate(apiData); const siteBriefing: SiteBriefingInfo = apiData.site_briefing ? { briefingCode: apiData.site_briefing.briefing_code || '', - partnerName: apiData.site_briefing.partner_name || '', - companyName: apiData.site_briefing.company_name || '', + partnerName: apiData.site_briefing.partner?.name || '', + companyName: '', briefingDate: apiData.site_briefing.briefing_date || '', - attendee: apiData.site_briefing.attendee || '', + attendee: '', } : { briefingCode: '', partnerName: '', companyName: '', briefingDate: '', attendee: '' }; - const bidInfo: BidInfo = apiData.bid_info - ? { - projectName: apiData.bid_info.project_name || '', - bidDate: apiData.bid_info.bid_date || '', - siteCount: apiData.bid_info.site_count || 0, - constructionPeriod: apiData.bid_info.construction_period || '', - constructionStartDate: apiData.bid_info.construction_start_date || '', - constructionEndDate: apiData.bid_info.construction_end_date || '', - vatType: apiData.bid_info.vat_type || 'excluded', - workReport: apiData.bid_info.work_report || '', - documents: (apiData.bid_info.documents || []).map((d) => ({ - id: String(d.id), - fileName: d.file_name || '', - fileUrl: d.file_url || '', - fileSize: d.file_size || 0, - })), - } - : { - projectName: '', - bidDate: '', - siteCount: 0, - constructionPeriod: '', - constructionStartDate: '', - constructionEndDate: '', - vatType: 'excluded', - workReport: '', - documents: [], - }; + const bidInfo: BidInfo = { + projectName: apiData.site_name || '', + bidDate: apiData.registration_date || '', + siteCount: 0, + constructionPeriod: '', + constructionStartDate: '', + constructionEndDate: '', + vatType: 'excluded', + workReport: '', + documents: [], + }; - const summaryItems: EstimateSummaryItem[] = (apiData.summary_items || []).map((item) => ({ - id: String(item.id), - name: item.name || '', - quantity: item.quantity || 0, - unit: item.unit || '', - materialCost: item.material_cost || 0, - laborCost: item.labor_cost || 0, - totalCost: item.total_cost || 0, - remarks: item.remarks || '', - })); + const summaryItems: EstimateSummaryItem[] = []; + const expenseItems: ExpenseItem[] = []; + const priceAdjustments: PriceAdjustmentItem[] = []; - const expenseItems: ExpenseItem[] = (apiData.expense_items || []).map((item) => ({ + const detailItems: EstimateDetailItem[] = (apiData.items || []).map((item, index) => ({ id: String(item.id), - name: item.name || '', - amount: item.amount || 0, - selected: item.selected || false, - })); - - const priceAdjustments: PriceAdjustmentItem[] = (apiData.price_adjustments || []).map((item) => ({ - id: String(item.id), - category: item.category || '', - unitPrice: item.unit_price || 0, - coating: item.coating || 0, - batting: item.batting || 0, - boxReinforce: item.box_reinforce || 0, - painting: item.painting || 0, - total: item.total || 0, - })); - - const detailItems: EstimateDetailItem[] = (apiData.detail_items || []).map((item) => ({ - id: String(item.id), - no: item.no || 0, - name: item.name || '', - material: item.material || '', - width: item.width || 0, - height: item.height || 0, - quantity: item.quantity || 0, - box: item.box || 0, - assembly: item.assembly || 0, - coating: item.coating || 0, - batting: item.batting || 0, - mounting: item.mounting || 0, - fitting: item.fitting || 0, - controller: item.controller || 0, - widthConstruction: item.width_construction || 0, - heightConstruction: item.height_construction || 0, - materialCost: item.material_cost || 0, - laborCost: item.labor_cost || 0, - quantityPrice: item.quantity_price || 0, - expenseQuantity: item.expense_quantity || 0, - expenseTotal: item.expense_total || 0, - totalCost: item.total_cost || 0, - otherCost: item.other_cost || 0, - marginCost: item.margin_cost || 0, + no: index + 1, + name: item.item_name || '', + material: item.specification || '', + width: 0, + height: 0, + quantity: item.calculated_quantity || 0, + box: 0, + assembly: 0, + coating: 0, + batting: 0, + mounting: 0, + fitting: 0, + controller: 0, + widthConstruction: 0, + heightConstruction: 0, + materialCost: 0, + laborCost: 0, + quantityPrice: item.unit_price || 0, + expenseQuantity: 0, + expenseTotal: 0, + totalCost: item.total_price || 0, + otherCost: 0, + marginCost: 0, totalPrice: item.total_price || 0, unitPrice: item.unit_price || 0, - expense: item.expense || 0, - marginRate: item.margin_rate || 0, - unitQuantity: item.unit_quantity || 0, - expenseResult: item.expense_result || 0, - marginActual: item.margin_actual || 0, + expense: 0, + marginRate: 0, + unitQuantity: item.base_quantity || 0, + expenseResult: 0, + marginActual: 0, })); return { @@ -300,45 +240,37 @@ function transformEstimateDetail(apiData: ApiEstimateDetail): EstimateDetail { function transformToApiRequest(data: Partial): Record { const apiData: Record = {}; - if (data.estimateCode !== undefined) apiData.estimate_code = data.estimateCode; - if (data.estimatorId !== undefined) apiData.estimator_id = data.estimatorId || null; - if (data.estimatorName !== undefined) apiData.estimator_name = data.estimatorName || null; - if (data.estimateAmount !== undefined) apiData.estimate_amount = data.estimateAmount; - if (data.status !== undefined) apiData.status = data.status; - - if (data.siteBriefing !== undefined) { - apiData.site_briefing = { - briefing_code: data.siteBriefing.briefingCode, - partner_name: data.siteBriefing.partnerName, - company_name: data.siteBriefing.companyName, - briefing_date: data.siteBriefing.briefingDate, - attendee: data.siteBriefing.attendee, + if (data.estimateCode !== undefined) apiData.quote_number = data.estimateCode; + if (data.estimatorId !== undefined) apiData.created_by = data.estimatorId || null; + if (data.estimatorName !== undefined) apiData.author = data.estimatorName || null; + if (data.estimateAmount !== undefined) apiData.total_amount = data.estimateAmount; + if (data.status !== undefined) { + // Estimate 상태 → Quote 상태 역매핑 + const reverseStatusMap: Record = { + pending: 'pending', + approval_waiting: 'sent', + completed: 'finalized', + rejected: 'rejected', + hold: 'draft', }; + apiData.status = reverseStatusMap[data.status] || 'pending'; } if (data.bidInfo !== undefined) { - apiData.bid_info = { - project_name: data.bidInfo.projectName, - bid_date: data.bidInfo.bidDate, - site_count: data.bidInfo.siteCount, - construction_period: data.bidInfo.constructionPeriod, - construction_start_date: data.bidInfo.constructionStartDate, - construction_end_date: data.bidInfo.constructionEndDate, - vat_type: data.bidInfo.vatType, - work_report: data.bidInfo.workReport, - }; + apiData.site_name = data.bidInfo.projectName; + apiData.registration_date = data.bidInfo.bidDate; } return apiData; } // ======================================== -// API 함수 +// API 함수 (quotes API 사용) // ======================================== /** - * 견적 목록 조회 - * GET /api/v1/estimates + * 건설 견적 목록 조회 + * GET /api/v1/quotes?quote_type=construction */ export async function getEstimateList(filter?: EstimateFilter): Promise<{ success: boolean; @@ -346,62 +278,77 @@ export async function getEstimateList(filter?: EstimateFilter): Promise<{ error?: string; }> { try { - const queryParams: Record = {}; + const queryParams: Record = { + quote_type: 'construction', // 건설 견적만 조회 + }; // 검색 - if (filter?.search) queryParams.search = filter.search; + if (filter?.search) queryParams.q = filter.search; // 필터 - if (filter?.status && filter.status !== 'all') queryParams.status = filter.status; - if (filter?.partnerId) queryParams.partner_id = filter.partnerId; - if (filter?.estimatorId) queryParams.estimator_id = filter.estimatorId; + if (filter?.status && filter.status !== 'all') { + // Estimate 상태 → Quote 상태로 변환 + const statusMap: Record = { + pending: 'pending', + approval_waiting: 'sent', + completed: 'finalized', + rejected: 'rejected', + hold: 'draft', + }; + queryParams.status = statusMap[filter.status] || filter.status; + } + if (filter?.partnerId) queryParams.client_id = filter.partnerId; // 날짜 범위 - if (filter?.startDate) queryParams.start_date = filter.startDate; - if (filter?.endDate) queryParams.end_date = filter.endDate; + if (filter?.startDate) queryParams.date_from = filter.startDate; + if (filter?.endDate) queryParams.date_to = filter.endDate; // 페이지네이션 if (filter?.page) queryParams.page = String(filter.page); - if (filter?.size) queryParams.per_page = String(filter.size); + if (filter?.size) queryParams.size = String(filter.size); // 정렬 if (filter?.sortBy) { const sortMap: Record = { latest: { field: 'created_at', dir: 'desc' }, oldest: { field: 'created_at', dir: 'asc' }, - amountDesc: { field: 'estimate_amount', dir: 'desc' }, - amountAsc: { field: 'estimate_amount', dir: 'asc' }, - bidDateDesc: { field: 'bid_date', dir: 'desc' }, - partnerNameAsc: { field: 'partner_name', dir: 'asc' }, - partnerNameDesc: { field: 'partner_name', dir: 'desc' }, - projectNameAsc: { field: 'project_name', dir: 'asc' }, - projectNameDesc: { field: 'project_name', dir: 'desc' }, + amountDesc: { field: 'total_amount', dir: 'desc' }, + amountAsc: { field: 'total_amount', dir: 'asc' }, + bidDateDesc: { field: 'registration_date', dir: 'desc' }, + partnerNameAsc: { field: 'client_name', dir: 'asc' }, + partnerNameDesc: { field: 'client_name', dir: 'desc' }, + projectNameAsc: { field: 'site_name', dir: 'asc' }, + projectNameDesc: { field: 'site_name', dir: 'desc' }, }; const sort = sortMap[filter.sortBy]; if (sort) { queryParams.sort_by = sort.field; - queryParams.sort_dir = sort.dir; + queryParams.sort_order = sort.dir; } } const response = await apiClient.get<{ - data: ApiEstimate[]; - current_page: number; - per_page: number; - total: number; - last_page: number; - }>('/estimates', { params: queryParams }); + success: boolean; + data: { + data: ApiQuote[]; + current_page: number; + per_page: number; + total: number; + last_page: number; + }; + }>('/quotes', { params: queryParams }); - const items = (response.data || []).map(transformEstimate); + const paginatedData = response.data; + const items = (paginatedData.data || []).map(transformQuoteToEstimate); return { success: true, data: { items, - total: response.total || 0, - page: response.current_page || 1, - size: response.per_page || 20, - totalPages: response.last_page || 1, + total: paginatedData.total || 0, + page: paginatedData.current_page || 1, + size: paginatedData.per_page || 20, + totalPages: paginatedData.last_page || 1, }, }; } catch (error) { @@ -412,7 +359,7 @@ export async function getEstimateList(filter?: EstimateFilter): Promise<{ /** * 견적 단건 조회 - * GET /api/v1/estimates/{id} + * GET /api/v1/quotes/{id} */ export async function getEstimate(id: string): Promise<{ success: boolean; @@ -420,8 +367,8 @@ export async function getEstimate(id: string): Promise<{ error?: string; }> { try { - const response = await apiClient.get(`/estimates/${id}`); - return { success: true, data: transformEstimate(response) }; + const response = await apiClient.get<{ success: boolean; data: ApiQuote }>(`/quotes/${id}`); + return { success: true, data: transformQuoteToEstimate(response.data) }; } catch (error) { console.error('견적 조회 오류:', error); return { success: false, error: '견적 정보를 찾을 수 없습니다.' }; @@ -430,7 +377,7 @@ export async function getEstimate(id: string): Promise<{ /** * 견적 상세 조회 (첨부 정보 포함) - * GET /api/v1/estimates/{id}/detail + * GET /api/v1/quotes/{id} */ export async function getEstimateDetail(id: string): Promise<{ success: boolean; @@ -438,8 +385,8 @@ export async function getEstimateDetail(id: string): Promise<{ error?: string; }> { try { - const response = await apiClient.get(`/estimates/${id}`); - return { success: true, data: transformEstimateDetail(response) }; + const response = await apiClient.get<{ success: boolean; data: ApiQuote }>(`/quotes/${id}`); + return { success: true, data: transformQuoteToEstimateDetail(response.data) }; } catch (error) { console.error('견적 상세 조회 오류:', error); return { success: false, error: '견적 상세 정보를 불러오는데 실패했습니다.' }; @@ -448,7 +395,8 @@ export async function getEstimateDetail(id: string): Promise<{ /** * 견적 통계 조회 - * GET /api/v1/estimates/stats + * GET /api/v1/quotes/stats (건설용) + * 현재는 목록 조회로 대체 */ export async function getEstimateStats(): Promise<{ success: boolean; @@ -456,14 +404,25 @@ export async function getEstimateStats(): Promise<{ error?: string; }> { try { - const response = await apiClient.get('/estimates/stats'); + // 통계 API가 없으므로 목록 조회로 대체 + const [allResponse, pendingResponse] = await Promise.all([ + apiClient.get<{ success: boolean; data: { total: number } }>('/quotes', { + params: { quote_type: 'construction', size: '1' }, + }), + apiClient.get<{ success: boolean; data: { total: number } }>('/quotes', { + params: { quote_type: 'construction', status: 'pending', size: '1' }, + }), + ]); + + const total = allResponse.data?.total || 0; + const pending = pendingResponse.data?.total || 0; return { success: true, data: { - total: response.total_count || 0, - pending: response.pending_count || 0, - completed: response.completed_count || 0, + total, + pending, + completed: total - pending, }, }; } catch (error) { @@ -474,7 +433,7 @@ export async function getEstimateStats(): Promise<{ /** * 견적 등록 - * POST /api/v1/estimates + * POST /api/v1/quotes */ export async function createEstimate(data: EstimateDetailFormData): Promise<{ success: boolean; @@ -482,9 +441,12 @@ export async function createEstimate(data: EstimateDetailFormData): Promise<{ error?: string; }> { try { - const apiData = transformToApiRequest(data); - const response = await apiClient.post('/estimates', apiData); - return { success: true, data: transformEstimate(response) }; + const apiData = { + ...transformToApiRequest(data), + quote_type: 'construction', // 건설 견적으로 생성 + }; + const response = await apiClient.post('/quotes', apiData); + return { success: true, data: transformQuoteToEstimate(response) }; } catch (error) { console.error('견적 등록 오류:', error); return { success: false, error: '견적 등록에 실패했습니다.' }; @@ -493,7 +455,7 @@ export async function createEstimate(data: EstimateDetailFormData): Promise<{ /** * 견적 수정 - * PUT /api/v1/estimates/{id} + * PUT /api/v1/quotes/{id} */ export async function updateEstimate( id: string, @@ -505,8 +467,8 @@ export async function updateEstimate( }> { try { const apiData = transformToApiRequest(data); - const response = await apiClient.put(`/estimates/${id}`, apiData); - return { success: true, data: transformEstimate(response) }; + const response = await apiClient.put(`/quotes/${id}`, apiData); + return { success: true, data: transformQuoteToEstimate(response) }; } catch (error) { console.error('견적 수정 오류:', error); return { success: false, error: '견적 수정에 실패했습니다.' }; @@ -515,14 +477,14 @@ export async function updateEstimate( /** * 견적 삭제 - * DELETE /api/v1/estimates/{id} + * DELETE /api/v1/quotes/{id} */ export async function deleteEstimate(id: string): Promise<{ success: boolean; error?: string; }> { try { - await apiClient.delete(`/estimates/${id}`); + await apiClient.delete(`/quotes/${id}`); return { success: true }; } catch (error) { console.error('견적 삭제 오류:', error); @@ -532,7 +494,7 @@ export async function deleteEstimate(id: string): Promise<{ /** * 견적 일괄 삭제 - * DELETE /api/v1/estimates/bulk + * DELETE /api/v1/quotes/bulk */ export async function deleteEstimates(ids: string[]): Promise<{ success: boolean; @@ -540,7 +502,7 @@ export async function deleteEstimates(ids: string[]): Promise<{ error?: string; }> { try { - await apiClient.delete('/estimates/bulk', { + await apiClient.delete('/quotes/bulk', { data: { ids: ids.map((id) => Number(id)) }, }); return { success: true, deletedCount: ids.length };