From 25795f8612f70653cdd8a1560f729c981d1ae2ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Tue, 3 Mar 2026 15:57:31 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[ai-quotation]=20=EC=A0=9C=EC=A1=B0=20?= =?UTF-8?q?=EA=B2=AC=EC=A0=81=EC=84=9C=20=EC=9E=90=EB=8F=99=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AI 2단계 분석: 고객 인터뷰 → 요구사항 추출 → 견적 산출 - 모델 확장: AiQuotation(모드/견적번호), AiQuotationItem(규격/단가/금액) - AiQuotePriceTable 모델 신규 생성 - Create 페이지: 모듈/제조 모드 탭, 제품 카테고리, 고객 정보 입력 - Show 페이지: 제조 모드 분기 렌더링 (품목/금액/고객정보) - Edit 페이지: 품목 인라인 편집, 할인/부가세/조건 입력 - Document: 한국 표준 제조업 견적서 양식 템플릿 - Controller/Route: update 엔드포인트, edit 라우트 추가 --- .../Api/Admin/Rd/AiQuotationController.php | 29 +- app/Http/Controllers/RdController.php | 22 + .../Requests/Rd/StoreAiQuotationRequest.php | 6 + app/Models/Rd/AiQuotation.php | 17 + app/Models/Rd/AiQuotationItem.php | 10 + app/Models/Rd/AiQuotePriceTable.php | 78 +++ app/Services/Rd/AiQuotationService.php | 484 +++++++++++++++++ .../views/rd/ai-quotation/create.blade.php | 154 +++++- .../manufacture-classic.blade.php | 257 +++++++++ .../views/rd/ai-quotation/document.blade.php | 159 +++--- .../views/rd/ai-quotation/edit.blade.php | 403 ++++++++++++++ .../views/rd/ai-quotation/show.blade.php | 498 ++++++++++++++---- routes/api.php | 1 + routes/web.php | 1 + 14 files changed, 1917 insertions(+), 202 deletions(-) create mode 100644 app/Models/Rd/AiQuotePriceTable.php create mode 100644 resources/views/rd/ai-quotation/document-templates/manufacture-classic.blade.php create mode 100644 resources/views/rd/ai-quotation/edit.blade.php diff --git a/app/Http/Controllers/Api/Admin/Rd/AiQuotationController.php b/app/Http/Controllers/Api/Admin/Rd/AiQuotationController.php index 4add3da7..be667c3a 100644 --- a/app/Http/Controllers/Api/Admin/Rd/AiQuotationController.php +++ b/app/Http/Controllers/Api/Admin/Rd/AiQuotationController.php @@ -90,7 +90,12 @@ public function analyze(int $id): JsonResponse ], 404); } - $result = $this->quotationService->runAnalysis($quotation); + // 제조 모드는 제조용 분석 실행 + if ($quotation->isManufacture()) { + $result = $this->quotationService->runManufactureAnalysis($quotation); + } else { + $result = $this->quotationService->runAnalysis($quotation); + } if ($result['ok']) { return response()->json([ @@ -106,4 +111,26 @@ public function analyze(int $id): JsonResponse 'error' => $result['error'], ], 422); } + + /** + * 견적 편집 저장 + */ + public function update(Request $request, int $id): JsonResponse + { + $result = $this->quotationService->updateQuotation($id, $request->all()); + + if ($result['ok']) { + return response()->json([ + 'success' => true, + 'message' => '견적이 저장되었습니다.', + 'data' => $result['quotation'], + ]); + } + + return response()->json([ + 'success' => false, + 'message' => '견적 저장에 실패했습니다.', + 'error' => $result['error'] ?? null, + ], 422); + } } diff --git a/app/Http/Controllers/RdController.php b/app/Http/Controllers/RdController.php index be9c34a0..ce5059db 100644 --- a/app/Http/Controllers/RdController.php +++ b/app/Http/Controllers/RdController.php @@ -91,4 +91,26 @@ public function showQuotation(Request $request, int $id): View|\Illuminate\Http\ return view('rd.ai-quotation.show', compact('quotation')); } + + /** + * AI 견적 편집 (제조 모드) + */ + public function editQuotation(Request $request, int $id): View|\Illuminate\Http\Response + { + if ($request->header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('rd.ai-quotation.edit', $id)); + } + + $quotation = $this->quotationService->getById($id); + + if (! $quotation) { + abort(404, 'AI 견적을 찾을 수 없습니다.'); + } + + if (! $quotation->isCompleted()) { + abort(403, '완료된 견적만 편집할 수 있습니다.'); + } + + return view('rd.ai-quotation.edit', compact('quotation')); + } } diff --git a/app/Http/Requests/Rd/StoreAiQuotationRequest.php b/app/Http/Requests/Rd/StoreAiQuotationRequest.php index aad91373..689e14af 100644 --- a/app/Http/Requests/Rd/StoreAiQuotationRequest.php +++ b/app/Http/Requests/Rd/StoreAiQuotationRequest.php @@ -18,6 +18,12 @@ public function rules(): array 'input_type' => 'required|in:text,voice,document', 'input_text' => 'required_if:input_type,text|nullable|string', 'ai_provider' => 'nullable|in:gemini,claude', + 'quote_mode' => 'nullable|in:module,manufacture', + 'product_category' => 'nullable|in:SCREEN,STEEL', + 'client_company' => 'nullable|string|max:200', + 'client_contact' => 'nullable|string|max:100', + 'client_phone' => 'nullable|string|max:50', + 'client_email' => 'nullable|email|max:200', ]; } diff --git a/app/Models/Rd/AiQuotation.php b/app/Models/Rd/AiQuotation.php index 44c03e81..be442b0a 100644 --- a/app/Models/Rd/AiQuotation.php +++ b/app/Models/Rd/AiQuotation.php @@ -17,6 +17,9 @@ class AiQuotation extends Model protected $fillable = [ 'tenant_id', + 'quote_mode', + 'quote_number', + 'product_category', 'title', 'input_type', 'input_text', @@ -49,6 +52,10 @@ class AiQuotation extends Model public const STATUS_FAILED = 'failed'; + public const MODE_MODULE = 'module'; + + public const MODE_MANUFACTURE = 'manufacture'; + public static function getStatuses(): array { return [ @@ -118,6 +125,16 @@ public function setOption(string $key, $value): void $this->save(); } + public function isManufacture(): bool + { + return $this->quote_mode === self::MODE_MANUFACTURE; + } + + public function isModule(): bool + { + return $this->quote_mode === self::MODE_MODULE; + } + public function getFormattedDevCostAttribute(): string { return number_format((int) $this->total_dev_cost).'원'; diff --git a/app/Models/Rd/AiQuotationItem.php b/app/Models/Rd/AiQuotationItem.php index 57b16c94..cfd5d81d 100644 --- a/app/Models/Rd/AiQuotationItem.php +++ b/app/Models/Rd/AiQuotationItem.php @@ -14,6 +14,13 @@ class AiQuotationItem extends Model 'module_id', 'module_code', 'module_name', + 'specification', + 'unit', + 'quantity', + 'unit_price', + 'total_price', + 'item_category', + 'floor_code', 'is_required', 'reason', 'dev_cost', @@ -27,6 +34,9 @@ class AiQuotationItem extends Model 'options' => 'array', 'dev_cost' => 'decimal:0', 'monthly_fee' => 'decimal:0', + 'quantity' => 'decimal:2', + 'unit_price' => 'decimal:2', + 'total_price' => 'decimal:2', ]; public function quotation(): BelongsTo diff --git a/app/Models/Rd/AiQuotePriceTable.php b/app/Models/Rd/AiQuotePriceTable.php new file mode 100644 index 00000000..2c63703b --- /dev/null +++ b/app/Models/Rd/AiQuotePriceTable.php @@ -0,0 +1,78 @@ + 'boolean', + 'options' => 'array', + 'min_value' => 'decimal:4', + 'max_value' => 'decimal:4', + 'unit_price' => 'decimal:2', + 'labor_rate' => 'decimal:2', + 'install_rate' => 'decimal:2', + ]; + + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + public function scopeByCategory($query, string $category) + { + return $query->where('product_category', $category); + } + + public static function getPriceTablesForPrompt(?string $productCategory = null): array + { + $query = static::active()->orderBy('product_category')->orderBy('min_value'); + + if ($productCategory) { + $query->byCategory($productCategory); + } + + return $query->get()->map(fn ($row) => [ + 'product_category' => $row->product_category, + 'price_type' => $row->price_type, + 'min_value' => (float) $row->min_value, + 'max_value' => (float) $row->max_value, + 'unit_price' => (float) $row->unit_price, + 'labor_rate' => (float) $row->labor_rate, + 'install_rate' => (float) $row->install_rate, + ])->toArray(); + } + + public function getOption(string $key, $default = null) + { + return data_get($this->options, $key, $default); + } + + public function setOption(string $key, $value): void + { + $options = $this->options ?? []; + data_set($options, $key, $value); + $this->options = $options; + $this->save(); + } +} diff --git a/app/Services/Rd/AiQuotationService.php b/app/Services/Rd/AiQuotationService.php index 71a85e01..e0a3b28f 100644 --- a/app/Services/Rd/AiQuotationService.php +++ b/app/Services/Rd/AiQuotationService.php @@ -6,9 +6,11 @@ use App\Models\Rd\AiQuotation; use App\Models\Rd\AiQuotationItem; use App\Models\Rd\AiQuotationModule; +use App\Models\Rd\AiQuotePriceTable; use App\Models\System\AiConfig; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; @@ -52,9 +54,12 @@ public function getById(int $id): ?AiQuotation public function createAndAnalyze(array $data): array { $provider = $data['ai_provider'] ?? 'gemini'; + $quoteMode = $data['quote_mode'] ?? AiQuotation::MODE_MODULE; $quotation = AiQuotation::create([ 'tenant_id' => session('selected_tenant_id', 1), + 'quote_mode' => $quoteMode, + 'product_category' => $data['product_category'] ?? null, 'title' => $data['title'], 'input_type' => $data['input_type'] ?? 'text', 'input_text' => $data['input_text'] ?? null, @@ -63,6 +68,23 @@ public function createAndAnalyze(array $data): array 'created_by' => Auth::id(), ]); + // 제조 모드: 고객 정보를 options에 저장 + if ($quoteMode === AiQuotation::MODE_MANUFACTURE) { + $quotation->update([ + 'quote_number' => $this->generateQuoteNumber($data['product_category'] ?? 'SC'), + 'options' => array_filter([ + 'client' => array_filter([ + 'company' => $data['client_company'] ?? null, + 'contact' => $data['client_contact'] ?? null, + 'phone' => $data['client_phone'] ?? null, + 'email' => $data['client_email'] ?? null, + ]), + ]), + ]); + + return $this->runManufactureAnalysis($quotation); + } + return $this->runAnalysis($quotation); } @@ -400,6 +422,468 @@ private function saveQuotationItems(AiQuotation $quotation, array $quotationResu } } + // =================================================== + // 제조 견적 (Manufacture) 전용 메서드 + // =================================================== + + /** + * 제조 견적 AI 분석 실행 + */ + public function runManufactureAnalysis(AiQuotation $quotation): array + { + try { + $quotation->update(['status' => AiQuotation::STATUS_PROCESSING]); + + $provider = $quotation->ai_provider; + $config = AiConfig::getActive($provider); + + if (! $config) { + throw new \RuntimeException("{$provider} API 설정이 없습니다."); + } + + $productCategory = $quotation->product_category ?? 'SCREEN'; + $priceTables = AiQuotePriceTable::getPriceTablesForPrompt($productCategory); + + // 1차: 요구사항 분석 + $analysisPrompt = $this->buildManufactureAnalysisPrompt( + $quotation->input_text, + $productCategory + ); + $analysisRaw = $this->callAi($config, $provider, $analysisPrompt, 'AI제조견적-분석'); + + $analysisResult = $this->parseJsonResponse($analysisRaw); + if (! $analysisResult) { + throw new \RuntimeException('AI 요구사항 분석 결과 파싱 실패'); + } + + $quotation->update(['analysis_result' => $analysisResult]); + + // 2차: 견적 산출 + $quotationPrompt = $this->buildManufactureQuotationPrompt( + $analysisResult, + $priceTables, + $productCategory + ); + $quotationRaw = $this->callAi($config, $provider, $quotationPrompt, 'AI제조견적-산출'); + + $quotationResult = $this->parseJsonResponse($quotationRaw); + if (! $quotationResult) { + throw new \RuntimeException('AI 견적 산출 결과 파싱 실패'); + } + + // 품목 저장 + $this->saveManufactureItems($quotation, $quotationResult); + + // 합계 계산 + options에 pricing 저장 + $totals = $quotation->items()->selectRaw( + 'SUM(total_price) as subtotal' + )->first(); + + $subtotal = (int) ($totals->subtotal ?? 0); + $pricing = $quotationResult['pricing'] ?? []; + $discountRate = (float) ($pricing['discount_rate'] ?? 0); + $discountAmount = (int) round($subtotal * $discountRate / 100); + $afterDiscount = $subtotal - $discountAmount; + $vatAmount = (int) round($afterDiscount * 0.1); + $finalAmount = $afterDiscount + $vatAmount; + + // 고객 정보 업데이트 (AI가 추출한 정보로 보강) + $clientFromAi = $analysisResult['client'] ?? []; + $existingOptions = $quotation->options ?? []; + $existingClient = $existingOptions['client'] ?? []; + $mergedClient = array_filter(array_merge($clientFromAi, $existingClient)); + + $existingOptions['client'] = $mergedClient; + $existingOptions['project'] = $analysisResult['project'] ?? []; + $existingOptions['pricing'] = [ + 'subtotal' => $subtotal, + 'material_cost' => (int) ($pricing['material_cost'] ?? 0), + 'labor_cost' => (int) ($pricing['labor_cost'] ?? 0), + 'install_cost' => (int) ($pricing['install_cost'] ?? 0), + 'discount_rate' => $discountRate, + 'discount_amount' => $discountAmount, + 'vat_amount' => $vatAmount, + 'final_amount' => $finalAmount, + ]; + $existingOptions['terms'] = $quotationResult['terms'] ?? [ + 'valid_until' => now()->addDays(30)->format('Y-m-d'), + 'payment' => '계약 시 50%, 설치 완료 후 50%', + 'delivery' => '계약 후 4주 이내', + ]; + + $quotation->update([ + 'quotation_result' => $quotationResult, + 'ai_model' => $config->model, + 'total_dev_cost' => $finalAmount, + 'total_monthly_fee' => 0, + 'options' => $existingOptions, + 'status' => AiQuotation::STATUS_COMPLETED, + ]); + + return [ + 'ok' => true, + 'quotation' => $quotation->fresh(['items', 'creator:id,name']), + ]; + } catch (\Exception $e) { + Log::error('AI 제조 견적 분석 실패', [ + 'quotation_id' => $quotation->id, + 'error' => $e->getMessage(), + ]); + + $quotation->update(['status' => AiQuotation::STATUS_FAILED]); + + return [ + 'ok' => false, + 'error' => $e->getMessage(), + 'quotation' => $quotation->fresh(), + ]; + } + } + + /** + * 견적번호 자동 생성 + */ + public function generateQuoteNumber(?string $productCategory): string + { + $prefix = match (strtoupper($productCategory ?? 'SC')) { + 'SCREEN' => 'SC', + 'STEEL' => 'ST', + default => 'SC', + }; + + $dateStr = now()->format('ymd'); + $baseNumber = "AQ-{$prefix}-{$dateStr}"; + + $count = AiQuotation::where('quote_number', 'like', "{$baseNumber}-%")->count(); + $seq = str_pad($count + 1, 2, '0', STR_PAD_LEFT); + + return "{$baseNumber}-{$seq}"; + } + + /** + * 제조 견적 품목 저장 + */ + private function saveManufactureItems(AiQuotation $quotation, array $quotationResult): void + { + $quotation->items()->delete(); + + $items = $quotationResult['items'] ?? []; + + foreach ($items as $index => $item) { + $quantity = (float) ($item['quantity'] ?? 1); + $unitPrice = (float) ($item['unit_price'] ?? 0); + $totalPrice = (float) ($item['total_price'] ?? ($quantity * $unitPrice)); + + AiQuotationItem::create([ + 'ai_quotation_id' => $quotation->id, + 'module_code' => $item['item_code'] ?? '', + 'module_name' => $item['item_name'] ?? '', + 'specification' => $item['specification'] ?? null, + 'unit' => $item['unit'] ?? 'SET', + 'quantity' => $quantity, + 'unit_price' => $unitPrice, + 'total_price' => $totalPrice, + 'item_category' => $item['item_category'] ?? 'material', + 'floor_code' => $item['floor_code'] ?? null, + 'reason' => $item['description'] ?? null, + 'sort_order' => $index, + ]); + } + } + + /** + * 견적 편집 저장 + */ + public function updateQuotation(int $id, array $data): array + { + $quotation = $this->getById($id); + + if (! $quotation) { + return ['ok' => false, 'error' => '견적을 찾을 수 없습니다.']; + } + + DB::beginTransaction(); + try { + // options 업데이트 + $options = $quotation->options ?? []; + + if (isset($data['client'])) { + $options['client'] = $data['client']; + } + if (isset($data['project'])) { + $options['project'] = $data['project']; + } + if (isset($data['terms'])) { + $options['terms'] = $data['terms']; + } + + // 품목 업데이트 + if (isset($data['items'])) { + $quotation->items()->delete(); + $subtotal = 0; + $materialCost = 0; + $laborCost = 0; + $installCost = 0; + + foreach ($data['items'] as $index => $item) { + $qty = (float) ($item['quantity'] ?? 1); + $price = (float) ($item['unit_price'] ?? 0); + $total = round($qty * $price, 2); + $subtotal += $total; + + $cat = $item['item_category'] ?? 'material'; + if ($cat === 'material') { + $materialCost += $total; + } elseif ($cat === 'labor') { + $laborCost += $total; + } elseif ($cat === 'install') { + $installCost += $total; + } + + AiQuotationItem::create([ + 'ai_quotation_id' => $quotation->id, + 'module_code' => $item['item_code'] ?? '', + 'module_name' => $item['item_name'] ?? '', + 'specification' => $item['specification'] ?? null, + 'unit' => $item['unit'] ?? 'SET', + 'quantity' => $qty, + 'unit_price' => $price, + 'total_price' => $total, + 'item_category' => $cat, + 'floor_code' => $item['floor_code'] ?? null, + 'reason' => $item['description'] ?? null, + 'sort_order' => $index, + ]); + } + + // 가격 재계산 + $discountRate = (float) ($data['discount_rate'] ?? $options['pricing']['discount_rate'] ?? 0); + $discountAmount = (int) round($subtotal * $discountRate / 100); + $afterDiscount = $subtotal - $discountAmount; + $vatAmount = (int) round($afterDiscount * 0.1); + $finalAmount = $afterDiscount + $vatAmount; + + $options['pricing'] = [ + 'subtotal' => (int) $subtotal, + 'material_cost' => (int) $materialCost, + 'labor_cost' => (int) $laborCost, + 'install_cost' => (int) $installCost, + 'discount_rate' => $discountRate, + 'discount_amount' => $discountAmount, + 'vat_amount' => $vatAmount, + 'final_amount' => $finalAmount, + ]; + + $quotation->update([ + 'total_dev_cost' => $finalAmount, + 'total_monthly_fee' => 0, + ]); + } + + $quotation->update(['options' => $options]); + + DB::commit(); + + return [ + 'ok' => true, + 'quotation' => $quotation->fresh(['items', 'creator:id,name']), + ]; + } catch (\Exception $e) { + DB::rollBack(); + Log::error('견적 편집 저장 실패', [ + 'quotation_id' => $id, + 'error' => $e->getMessage(), + ]); + + return ['ok' => false, 'error' => $e->getMessage()]; + } + } + + /** + * 금액을 한글로 변환 + */ + public static function numberToKorean(int $number): string + { + if ($number === 0) { + return '영'; + } + + $units = ['', '만', '억', '조']; + $digits = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구']; + $subUnits = ['', '십', '백', '천']; + + $result = ''; + $unitIndex = 0; + + while ($number > 0) { + $chunk = $number % 10000; + if ($chunk > 0) { + $chunkStr = ''; + $subIndex = 0; + $temp = $chunk; + while ($temp > 0) { + $digit = $temp % 10; + if ($digit > 0) { + $prefix = ($digit === 1 && $subIndex > 0) ? '' : $digits[$digit]; + $chunkStr = $prefix.$subUnits[$subIndex].$chunkStr; + } + $temp = (int) ($temp / 10); + $subIndex++; + } + $result = $chunkStr.$units[$unitIndex].$result; + } + $number = (int) ($number / 10000); + $unitIndex++; + } + + return $result; + } + + // =================================================== + // 제조 견적 AI 프롬프트 + // =================================================== + + /** + * 제조 견적용 1단계: 요구사항 분석 + */ + private function buildManufactureAnalysisPrompt(string $interviewText, string $productCategory): string + { + $categoryLabel = $productCategory === 'STEEL' ? '철재(방화문/방화셔터)' : '방화스크린'; + + return <<

인터뷰 내용 입력

-

고객사 인터뷰 내용을 입력하면 AI가 업무를 분석하고 맞춤형 견적서를 자동 생성합니다.

+

고객사 인터뷰 내용을 입력하면 AI가 분석하고 맞춤형 견적서를 자동 생성합니다.

+ +
+ +
+ + +
+
+ + + +
@@ -101,6 +159,45 @@ class="px-6 py-2.5 bg-purple-600 text-white rounded-lg hover:bg-purple-700 trans @push('scripts') @endpush diff --git a/resources/views/rd/ai-quotation/document-templates/manufacture-classic.blade.php b/resources/views/rd/ai-quotation/document-templates/manufacture-classic.blade.php new file mode 100644 index 00000000..9dacdae1 --- /dev/null +++ b/resources/views/rd/ai-quotation/document-templates/manufacture-classic.blade.php @@ -0,0 +1,257 @@ +{{-- 제조 견적서 클래식 템플릿 — 한국 표준 제조업 견적서 양식 --}} + + +@php + $materialItems = $quotation->items->where('item_category', 'material'); + $laborItems = $quotation->items->where('item_category', 'labor'); + $installItems = $quotation->items->where('item_category', 'install'); + $otherItems = $quotation->items->whereNotIn('item_category', ['material', 'labor', 'install']); + $allItems = $materialItems->merge($laborItems)->merge($installItems)->merge($otherItems); + + $materialTotal = $materialItems->sum('total_price'); + $laborTotal = $laborItems->sum('total_price'); + $installTotal = $installItems->sum('total_price'); +@endphp + +
+ + {{-- 제목 --}} +

+ 견 적 서 +

+ + {{-- 견적 정보 --}} +
+
+

견적번호: {{ $quotationNo }}

+

유효기간: {{ $validUntil }}

+
+
+

견적일자: {{ $quotation->created_at->format('Y년 m월 d일') }}

+

제품구분: {{ $quotation->product_category === 'STEEL' ? '철재' : '방화스크린' }}

+
+
+ + {{-- 수신 / 공급자 --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + @if(!empty($project['name'])) + + + + @endif + +
수 신공 급 자
상 호{{ $client['company'] ?? $quotation->title }}상 호(주)코드브릿지엑스
담당자{{ $client['contact'] ?? '-' }}대 표이의찬
연락처{{ $client['phone'] ?? '-' }}주 소인천 남동구 남동대로 215번길 30
이메일{{ $client['email'] ?? '-' }}연락처032-123-4567
현장명{{ $project['name'] }}{{ !empty($project['location']) ? ' ('.$project['location'].')' : '' }}
+ + {{-- 합계 --}} +
+

아래와 같이 견적합니다.

+

+ 합계금액: 금 {{ $subtotalKorean }}원정 + (₩{{ number_format($subtotal) }}) +

+

※ 부가가치세 별도

+
+ + {{-- 품목 테이블 --}} + + + + + + + + + + + + + + + + + + + + @php $no = 1; @endphp + + {{-- 재료비 --}} + @if($materialItems->isNotEmpty()) + + + + @foreach($materialItems as $item) + + + + + + + + + + + @endforeach + @endif + + {{-- 노무비 --}} + @if($laborItems->isNotEmpty()) + + + + @foreach($laborItems as $item) + + + + + + + + + + + @endforeach + @endif + + {{-- 설치비 --}} + @if($installItems->isNotEmpty()) + + + + @foreach($installItems as $item) + + + + + + + + + + + @endforeach + @endif + + {{-- 기타 --}} + @foreach($otherItems as $item) + + + + + + + + + + + @endforeach + + + {{-- 카테고리별 소계 --}} + @if($materialItems->isNotEmpty()) + + + + + @endif + @if($laborItems->isNotEmpty()) + + + + + @endif + @if($installItems->isNotEmpty()) + + + + + @endif + + + + + @if($discountAmount > 0) + + + + + @endif + + + + + + + + + +
No위치품 목규 격단위수량단가 (원)금액 (원)
[ 재료비 ]
{{ $no++ }}{{ $item->floor_code }}{{ $item->module_name }}{{ $item->specification }}{{ $item->unit }}{{ number_format((float)$item->quantity, 0) }}{{ number_format((float)$item->unit_price) }}{{ number_format((float)$item->total_price) }}
[ 노무비 ]
{{ $no++ }}{{ $item->floor_code }}{{ $item->module_name }}{{ $item->specification }}{{ $item->unit }}{{ number_format((float)$item->quantity, 0) }}{{ number_format((float)$item->unit_price) }}{{ number_format((float)$item->total_price) }}
[ 설치비 ]
{{ $no++ }}{{ $item->floor_code }}{{ $item->module_name }}{{ $item->specification }}{{ $item->unit }}{{ number_format((float)$item->quantity, 0) }}{{ number_format((float)$item->unit_price) }}{{ number_format((float)$item->total_price) }}
{{ $no++ }}{{ $item->floor_code }}{{ $item->module_name }}{{ $item->specification }}{{ $item->unit }}{{ number_format((float)$item->quantity, 0) }}{{ number_format((float)$item->unit_price) }}{{ number_format((float)$item->total_price) }}
재료비 소계{{ number_format((int)$materialTotal) }}
노무비 소계{{ number_format((int)$laborTotal) }}
설치비 소계{{ number_format((int)$installTotal) }}
소 계{{ number_format($subtotal) }}
할인 ({{ $discountRate }}%)-{{ number_format($discountAmount) }}
부가세 (10%){{ number_format($vatAmount) }}
합 계{{ number_format($finalAmount) }}
+ + {{-- 비고 / 조건 --}} +
+ + + + + + + +
비 고
+
    +
  1. 상기 금액은 부가가치세 별도입니다.
  2. +
  3. 결제 조건: {{ $terms['payment'] ?? '계약 시 50%, 설치 완료 후 50%' }}
  4. +
  5. 납기 조건: {{ $terms['delivery'] ?? '계약 후 4주 이내' }}
  6. +
  7. 본 견적서의 유효기간: {{ $validUntil }}까지
  8. +
  9. 설치 현장 여건에 따라 규격 및 금액이 변경될 수 있습니다.
  10. +
  11. 세부 사항은 별도 협의를 통해 조정될 수 있습니다.
  12. +
+
+
+ + {{-- 서명 --}} +
+
+

(주)코드브릿지엑스

+

대표이사 이 의 찬

+
+
(인)
+
+ +
diff --git a/resources/views/rd/ai-quotation/document.blade.php b/resources/views/rd/ai-quotation/document.blade.php index d75cb76a..06ce3cc5 100644 --- a/resources/views/rd/ai-quotation/document.blade.php +++ b/resources/views/rd/ai-quotation/document.blade.php @@ -30,72 +30,63 @@ @section('content') @php - // 견적번호 - $quotationNo = 'AQ-' . $quotation->created_at->format('Y') . '-' . str_pad($quotation->id, 3, '0', STR_PAD_LEFT); - - // 회사 분석 정보 - $company = $quotation->analysis_result['company_analysis'] ?? []; - - // 구현 계획 - $plan = $quotation->quotation_result['implementation_plan'] ?? []; - $estimatedMonths = $plan['estimated_months'] ?? null; - - // 금액 계산 - $devSubtotal = (int) $quotation->total_dev_cost; - $monthlySubtotal = (int) $quotation->total_monthly_fee; - $devVat = (int) round($devSubtotal * 0.1); - $monthlyVat = (int) round($monthlySubtotal * 0.1); - $devTotal = $devSubtotal + $devVat; - $monthlyTotal = $monthlySubtotal + $monthlyVat; - - // 한글 금액 변환 + // 한글 금액 변환 함수 if (!function_exists('numberToKorean')) { function numberToKorean(int $number): string { - if ($number === 0) return '영'; - $units = ['', '만', '억', '조']; - $digits = ['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구']; - $subUnits = ['', '십', '백', '천']; - - $result = ''; - $unitIndex = 0; - while ($number > 0) { - $chunk = $number % 10000; - if ($chunk > 0) { - $chunkStr = ''; - $subIndex = 0; - $temp = $chunk; - while ($temp > 0) { - $digit = $temp % 10; - if ($digit > 0) { - $prefix = ($digit === 1 && $subIndex > 0) ? '' : $digits[$digit]; - $chunkStr = $prefix . $subUnits[$subIndex] . $chunkStr; - } - $temp = (int)($temp / 10); - $subIndex++; - } - $result = $chunkStr . $units[$unitIndex] . $result; - } - $number = (int)($number / 10000); - $unitIndex++; - } - return $result; + return \App\Services\Rd\AiQuotationService::numberToKorean($number); } } - $devTotalKorean = numberToKorean($devSubtotal); - - // 필수 → 선택 순으로 정렬된 품목 - $sortedItems = $quotation->items->sortByDesc('is_required')->values(); + // 견적 모드 판별 + $isManufacture = $quotation->isManufacture(); // 템플릿 (기본값: classic) $template = $template ?? 'classic'; - $templates = [ - 'classic' => '클래식', - 'modern' => '모던', - 'blue' => '블루', - 'dark' => '다크', - 'colorful' => '컬러풀', - ]; + + if ($isManufacture) { + // 제조 견적서 데이터 + $options = $quotation->options ?? []; + $client = $options['client'] ?? []; + $project = $options['project'] ?? []; + $pricing = $options['pricing'] ?? []; + $terms = $options['terms'] ?? []; + + $quotationNo = $quotation->quote_number ?? 'AQ-'.$quotation->created_at->format('Y').'-'.str_pad($quotation->id, 3, '0', STR_PAD_LEFT); + $subtotal = (int) ($pricing['subtotal'] ?? 0); + $discountRate = (float) ($pricing['discount_rate'] ?? 0); + $discountAmount = (int) ($pricing['discount_amount'] ?? 0); + $vatAmount = (int) ($pricing['vat_amount'] ?? 0); + $finalAmount = (int) ($pricing['final_amount'] ?? 0); + $subtotalKorean = numberToKorean($subtotal); + $validUntil = $terms['valid_until'] ?? now()->addDays(30)->format('Y-m-d'); + + $templates = [ + 'classic' => '클래식', + ]; + } else { + // 모듈 추천 견적서 데이터 (기존) + $quotationNo = 'AQ-' . $quotation->created_at->format('Y') . '-' . str_pad($quotation->id, 3, '0', STR_PAD_LEFT); + $company = $quotation->analysis_result['company_analysis'] ?? []; + $plan = $quotation->quotation_result['implementation_plan'] ?? []; + $estimatedMonths = $plan['estimated_months'] ?? null; + + $devSubtotal = (int) $quotation->total_dev_cost; + $monthlySubtotal = (int) $quotation->total_monthly_fee; + $devVat = (int) round($devSubtotal * 0.1); + $monthlyVat = (int) round($monthlySubtotal * 0.1); + $devTotal = $devSubtotal + $devVat; + $monthlyTotal = $monthlySubtotal + $monthlyVat; + $devTotalKorean = numberToKorean($devSubtotal); + $sortedItems = $quotation->items->sortByDesc('is_required')->values(); + + $templates = [ + 'classic' => '클래식', + 'modern' => '모던', + 'blue' => '블루', + 'dark' => '다크', + 'colorful' => '컬러풀', + ]; + } @endphp {{-- 템플릿 선택 UI (인쇄 시 숨김) --}} @@ -109,21 +100,45 @@ class="template-card {{ $template === $key ? 'active' : '' }}"> @endforeach
+ {{-- PDF 다운로드 버튼 --}} +
+ +
{{-- 선택된 템플릿 렌더링 --}} -@include('rd.ai-quotation.document-templates.' . $template, [ - 'quotation' => $quotation, - 'quotationNo' => $quotationNo, - 'company' => $company, - 'estimatedMonths' => $estimatedMonths, - 'devSubtotal' => $devSubtotal, - 'monthlySubtotal' => $monthlySubtotal, - 'devVat' => $devVat, - 'monthlyVat' => $monthlyVat, - 'devTotal' => $devTotal, - 'monthlyTotal' => $monthlyTotal, - 'devTotalKorean' => $devTotalKorean, - 'sortedItems' => $sortedItems, -]) +@if($isManufacture) + @include('rd.ai-quotation.document-templates.manufacture-classic', [ + 'quotation' => $quotation, + 'quotationNo' => $quotationNo, + 'client' => $client, + 'project' => $project, + 'pricing' => $pricing, + 'terms' => $terms, + 'subtotal' => $subtotal, + 'discountRate' => $discountRate, + 'discountAmount' => $discountAmount, + 'vatAmount' => $vatAmount, + 'finalAmount' => $finalAmount, + 'subtotalKorean' => $subtotalKorean, + 'validUntil' => $validUntil, + ]) +@else + @include('rd.ai-quotation.document-templates.' . $template, [ + 'quotation' => $quotation, + 'quotationNo' => $quotationNo, + 'company' => $company, + 'estimatedMonths' => $estimatedMonths, + 'devSubtotal' => $devSubtotal, + 'monthlySubtotal' => $monthlySubtotal, + 'devVat' => $devVat, + 'monthlyVat' => $monthlyVat, + 'devTotal' => $devTotal, + 'monthlyTotal' => $monthlyTotal, + 'devTotalKorean' => $devTotalKorean, + 'sortedItems' => $sortedItems, + ]) +@endif @endsection diff --git a/resources/views/rd/ai-quotation/edit.blade.php b/resources/views/rd/ai-quotation/edit.blade.php new file mode 100644 index 00000000..e8caa8d5 --- /dev/null +++ b/resources/views/rd/ai-quotation/edit.blade.php @@ -0,0 +1,403 @@ +@extends('layouts.app') + +@section('title', '견적서 편집') + +@section('content') +@php + $options = $quotation->options ?? []; + $client = $options['client'] ?? []; + $project = $options['project'] ?? []; + $pricing = $options['pricing'] ?? []; + $terms = $options['terms'] ?? []; +@endphp + + +
+

+ + 견적서 편집 + {{ $quotation->quote_number }} +

+ +
+ + + +
+
+

+ 고객 정보 +

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+

+ 프로젝트 정보 +

+
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+

+ 품목 편집 +

+ +
+
+ + + + + + + + + + + + + + + + + @foreach($quotation->items as $index => $item) + + + + + + + + + + + + + @endforeach + + + + + + + + +
No분류위치품목명규격단위수량단가금액
{{ $index + 1 }} + + + + + + + + + + + + + + + {{ number_format((float)$item->total_price) }} + + +
소계{{ number_format((int)($pricing['subtotal'] ?? 0)) }}
+
+
+ + +
+ +
+
+

+ 가격 조정 +

+
+
+
+ + +
+
+
소계{{ number_format((int)($pricing['subtotal'] ?? 0)) }}원
+
할인-{{ number_format((int)($pricing['discount_amount'] ?? 0)) }}원
+
부가세 (10%){{ number_format((int)($pricing['vat_amount'] ?? 0)) }}원
+
+ 최종 금액 + {{ number_format((int)($pricing['final_amount'] ?? 0)) }}원 +
+
+
+
+ + +
+
+

+ 견적 조건 +

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+ + 취소 + + +
+
+@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/rd/ai-quotation/show.blade.php b/resources/views/rd/ai-quotation/show.blade.php index ecacee15..37203473 100644 --- a/resources/views/rd/ai-quotation/show.blade.php +++ b/resources/views/rd/ai-quotation/show.blade.php @@ -11,12 +11,21 @@ {{ $quotation->title }} {{ $quotation->status_label }} + @if($quotation->isManufacture()) + 제조 견적 + @endif -
-

입력 유형

-

{{ ['text' => '텍스트', 'voice' => '음성', 'document' => '문서'][$quotation->input_type] ?? $quotation->input_type }}

-
+ @if($quotation->isManufacture()) +
+

견적번호

+

{{ $quotation->quote_number ?? '-' }}

+
+
+

제품 카테고리

+

{{ $quotation->product_category === 'STEEL' ? '철재' : '방화스크린' }}

+
+ @else +
+

입력 유형

+

{{ ['text' => '텍스트', 'voice' => '음성', 'document' => '문서'][$quotation->input_type] ?? $quotation->input_type }}

+
+ @endif

요청자

{{ $quotation->creator?->name ?? '-' }}

@@ -54,46 +74,297 @@ class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transit
@if($quotation->isCompleted()) - - @if($quotation->analysis_result) - @php $analysis = $quotation->analysis_result; @endphp + @if($quotation->isManufacture()) + {{-- ============================================ --}} + {{-- 제조 견적 모드 --}} + {{-- ============================================ --}} + + @php + $options = $quotation->options ?? []; + $client = $options['client'] ?? []; + $project = $options['project'] ?? []; + $pricing = $options['pricing'] ?? []; + $terms = $options['terms'] ?? []; + $analysis = $quotation->analysis_result ?? []; + $productSpecs = $analysis['product_specs'] ?? []; + @endphp + + + @if(!empty($client))

- AI 업무 분석 결과 + 고객 정보

- - @if(isset($analysis['company_analysis'])) - @php $company = $analysis['company_analysis']; @endphp -
-
- 업종 - {{ $company['industry'] ?? '-' }} -
-
- 규모 - {{ $company['scale'] ?? '-' }} -
-
- 디지털화 수준 - {{ $company['digitalization_level'] ?? '-' }} -
- @if(!empty($company['current_systems'])) +
+
+

회사명

+

{{ $client['company'] ?? '-' }}

+
+
+

담당자

+

{{ $client['contact'] ?? '-' }}

+
+
+

연락처

+

{{ $client['phone'] ?? '-' }}

+
+
+

이메일

+

{{ $client['email'] ?? '-' }}

+
+
+
+
+ @endif + + + @if(!empty($project)) +
+
+

+ 프로젝트 분석 +

+
+
+
+
+ 현장명 + {{ $project['name'] ?? '-' }} +
+
+ 건물유형 + {{ $project['building_type'] ?? '-' }} +
+ @if(!empty($project['location'])) +
+ 위치 + {{ $project['location'] }} +
+ @endif +
+
+
+ @endif + + + @if(!empty($productSpecs)) +
+
+

+ 위치별 제품 사양 (AI 추출) +

+
+
+ + + + + + + + + + + + + + @foreach($productSpecs as $spec) + + + + + + + + + + @endforeach + +
위치유형규격 (W×H)수량가이드레일모터비고
+
{{ $spec['floor_name'] ?? '-' }}
+
{{ $spec['floor_code'] ?? '' }}
+
+ {{ $spec['product_type'] ?? '-' }} + {{ ($spec['width_mm'] ?? 0) }}×{{ ($spec['height_mm'] ?? 0) }}{{ $spec['quantity'] ?? 1 }}{{ $spec['guide_rail'] ?? '-' }}{{ $spec['motor'] ?? '-' }}{{ $spec['note'] ?? '' }}
+
+
+ @endif + + +
+
+

+ 견적 품목 +

+
+
+ @php + $materialItems = $quotation->items->where('item_category', 'material'); + $laborItems = $quotation->items->where('item_category', 'labor'); + $installItems = $quotation->items->where('item_category', 'install'); + $otherItems = $quotation->items->whereNotIn('item_category', ['material', 'labor', 'install']); + @endphp + + + + + + + + + + + + + + @php $no = 1; @endphp + + {{-- 재료비 --}} + @if($materialItems->isNotEmpty()) + + + + @foreach($materialItems as $item) + + + + + + + + + + @endforeach + @endif + + {{-- 노무비 --}} + @if($laborItems->isNotEmpty()) + + + + @foreach($laborItems as $item) + + + + + + + + + + @endforeach + @endif + + {{-- 설치비 --}} + @if($installItems->isNotEmpty()) + + + + @foreach($installItems as $item) + + + + + + + + + + @endforeach + @endif + + {{-- 기타 --}} + @foreach($otherItems as $item) + + + + + + + + + + @endforeach + +
No위치품목규격수량단가금액
재료비
{{ $no++ }}{{ $item->floor_code }}{{ $item->module_name }}{{ $item->specification }}{{ number_format((float)$item->quantity, 0) }} {{ $item->unit }}{{ number_format((float)$item->unit_price) }}{{ number_format((float)$item->total_price) }}
노무비
{{ $no++ }}{{ $item->floor_code }}{{ $item->module_name }}{{ $item->specification }}{{ number_format((float)$item->quantity, 0) }} {{ $item->unit }}{{ number_format((float)$item->unit_price) }}{{ number_format((float)$item->total_price) }}
설치비
{{ $no++ }}{{ $item->floor_code }}{{ $item->module_name }}{{ $item->specification }}{{ number_format((float)$item->quantity, 0) }} {{ $item->unit }}{{ number_format((float)$item->unit_price) }}{{ number_format((float)$item->total_price) }}
{{ $no++ }}{{ $item->floor_code }}{{ $item->module_name }}{{ $item->specification }}{{ number_format((float)$item->quantity, 0) }} {{ $item->unit }}{{ number_format((float)$item->unit_price) }}{{ number_format((float)$item->total_price) }}
+
+
+ + +
+

+ 금액 요약 +

+
+
+
재료비{{ number_format((int)($pricing['material_cost'] ?? 0)) }}원
+
노무비{{ number_format((int)($pricing['labor_cost'] ?? 0)) }}원
+
설치비{{ number_format((int)($pricing['install_cost'] ?? 0)) }}원
+
소계{{ number_format((int)($pricing['subtotal'] ?? 0)) }}원
+
+
+ @if(($pricing['discount_rate'] ?? 0) > 0) +
할인 ({{ $pricing['discount_rate'] }}%)-{{ number_format((int)($pricing['discount_amount'] ?? 0)) }}원
+ @endif +
부가세 (10%){{ number_format((int)($pricing['vat_amount'] ?? 0)) }}원
+
+ 최종 금액 + {{ number_format((int)($pricing['final_amount'] ?? 0)) }}원 +
+ @php + $koreanAmount = \App\Services\Rd\AiQuotationService::numberToKorean((int)($pricing['subtotal'] ?? 0)); + @endphp +
금 {{ $koreanAmount }}원정 (VAT 별도)
+
+
+
+ + @else + {{-- ============================================ --}} + {{-- 모듈 추천 모드 (기존) --}} + {{-- ============================================ --}} + + + @if($quotation->analysis_result) + @php $analysis = $quotation->analysis_result; @endphp +
+
+

+ AI 업무 분석 결과 +

+
+
+ @if(isset($analysis['company_analysis'])) + @php $company = $analysis['company_analysis']; @endphp +
+
+ 업종 + {{ $company['industry'] ?? '-' }} +
+
+ 규모 + {{ $company['scale'] ?? '-' }} +
+
+ 디지털화 수준 + {{ $company['digitalization_level'] ?? '-' }} +
+ @if(!empty($company['current_systems']))
현재 시스템 {{ implode(', ', $company['current_systems']) }}
- @endif -
- @endif + @endif +
+ @endif - - @if(!empty($analysis['business_domains'])) -

업무 영역 분석

-
- @foreach($analysis['business_domains'] as $domain) + @if(!empty($analysis['business_domains'])) +

업무 영역 분석

+
+ @foreach($analysis['business_domains'] as $domain)

{{ $domain['domain'] ?? '' }}

@@ -109,47 +380,47 @@ class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transit

{{ $domain['current_process'] ?? '' }}

@if(!empty($domain['pain_points'])) -
- @foreach($domain['pain_points'] as $point) - {{ $point }} - @endforeach -
+
+ @foreach($domain['pain_points'] as $point) + {{ $point }} + @endforeach +
@endif @if(!empty($domain['matched_modules'])) -
- @foreach($domain['matched_modules'] as $mod) - {{ $mod }} - @endforeach -
+
+ @foreach($domain['matched_modules'] as $mod) + {{ $mod }} + @endforeach +
@endif
- @endforeach -
- @endif + @endforeach +
+ @endif +
- - @endif + @endif - -
-
-

- 추천 모듈 및 견적 -

-
-
- - - - - - - - - - - - @foreach($quotation->items as $item) + +
+
+

+ 추천 모듈 및 견적 +

+
+
+
구분모듈추천 근거개발비월 구독료
+ + + + + + + + + + + @foreach($quotation->items as $item) - @endforeach - - - - - - - - -
구분모듈추천 근거개발비월 구독료
@if($item->is_required) @@ -168,69 +439,69 @@ class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transit {{ number_format((int)$item->dev_cost) }}원 {{ number_format((int)$item->monthly_fee) }}원
합계{{ number_format((int)$quotation->total_dev_cost) }}원{{ number_format((int)$quotation->total_monthly_fee) }}원/월
-
-
- - - @if(!empty($quotation->quotation_result['implementation_plan'])) - @php $plan = $quotation->quotation_result['implementation_plan']; @endphp -
-
-

- 구현 계획 (AI 추천) -

+ @endforeach + + + + 합계 + {{ number_format((int)$quotation->total_dev_cost) }}원 + {{ number_format((int)$quotation->total_monthly_fee) }}원/월 + + +
-
-

예상 기간: {{ $plan['estimated_months'] ?? '?' }}개월

- @if(!empty($plan['phases'])) +
+ + + @if(!empty($quotation->quotation_result['implementation_plan'])) + @php $plan = $quotation->quotation_result['implementation_plan']; @endphp +
+
+

+ 구현 계획 (AI 추천) +

+
+
+

예상 기간: {{ $plan['estimated_months'] ?? '?' }}개월

+ @if(!empty($plan['phases']))
@foreach($plan['phases'] as $phase) -
-
- {{ $phase['phase'] ?? '' }} -
-
-

{{ $phase['name'] ?? '' }}

-
- {{ $phase['duration_weeks'] ?? '?' }}주 - @if(!empty($phase['modules'])) - | - @foreach($phase['modules'] as $mod) - {{ $mod }} - @endforeach - @endif -
+
+
+ {{ $phase['phase'] ?? '' }} +
+
+

{{ $phase['name'] ?? '' }}

+
+ {{ $phase['duration_weeks'] ?? '?' }}주 + @if(!empty($phase['modules'])) + | + @foreach($phase['modules'] as $mod) + {{ $mod }} + @endforeach + @endif
+
@endforeach
- @endif + @endif +
-
- @endif + @endif - - @if(!empty($quotation->quotation_result['analysis_summary'])) + + @if(!empty($quotation->quotation_result['analysis_summary']))

AI 분석 요약

{{ $quotation->quotation_result['analysis_summary'] }}

+ @endif @endif @elseif($quotation->status === 'failed') -
@@ -240,7 +511,6 @@ class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg transit
@elseif($quotation->isProcessing()) -

AI 분석 진행중...

diff --git a/routes/api.php b/routes/api.php index f3bc0cb4..c035daea 100644 --- a/routes/api.php +++ b/routes/api.php @@ -612,6 +612,7 @@ Route::get('/', [\App\Http\Controllers\Api\Admin\Rd\AiQuotationController::class, 'index'])->name('index'); Route::post('/', [\App\Http\Controllers\Api\Admin\Rd\AiQuotationController::class, 'store'])->name('store'); Route::get('/{id}', [\App\Http\Controllers\Api\Admin\Rd\AiQuotationController::class, 'show'])->name('show'); + Route::put('/{id}', [\App\Http\Controllers\Api\Admin\Rd\AiQuotationController::class, 'update'])->name('update'); Route::post('/{id}/analyze', [\App\Http\Controllers\Api\Admin\Rd\AiQuotationController::class, 'analyze'])->name('analyze'); }); }); diff --git a/routes/web.php b/routes/web.php index e4db4003..b40847d9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -376,6 +376,7 @@ Route::get('/ai-quotation', [RdController::class, 'quotations'])->name('ai-quotation.index'); Route::get('/ai-quotation/create', [RdController::class, 'createQuotation'])->name('ai-quotation.create'); Route::get('/ai-quotation/{id}/document', [RdController::class, 'documentQuotation'])->name('ai-quotation.document'); + Route::get('/ai-quotation/{id}/edit', [RdController::class, 'editQuotation'])->name('ai-quotation.edit'); Route::get('/ai-quotation/{id}', [RdController::class, 'showQuotation'])->name('ai-quotation.show'); });