diff --git a/app/Http/Controllers/Sales/SalesScenarioController.php b/app/Http/Controllers/Sales/SalesScenarioController.php index d2ec9559..a5235b28 100644 --- a/app/Http/Controllers/Sales/SalesScenarioController.php +++ b/app/Http/Controllers/Sales/SalesScenarioController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Models\Sales\SalesScenarioChecklist; use App\Models\Sales\SalesTenantManagement; +use App\Models\Sales\TenantProspect; use App\Models\Tenants\Tenant; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -164,4 +165,158 @@ public function getProgress(int $tenantId, string $type): JsonResponse 'progress' => $progress, ]); } + + // ============================================ + // 가망고객(Prospect) 기반 메서드 + // ============================================ + + /** + * 가망고객 영업 시나리오 모달 뷰 + */ + public function prospectSalesScenario(int $prospectId, Request $request): View|Response + { + $prospect = TenantProspect::findOrFail($prospectId); + $steps = config('sales_scenario.sales_steps'); + $currentStep = (int) $request->input('step', 1); + $icons = config('sales_scenario.icons'); + + // 가망고객 영업 관리 정보 조회 또는 생성 + $management = SalesTenantManagement::findOrCreateByProspect($prospectId); + + // 체크리스트 진행 상태 조회 (DB에서) + $progress = SalesScenarioChecklist::calculateProgressByProspect($prospectId, 'sales', $steps); + + // 진행률 업데이트 + $management->updateProgress('sales', $progress['percentage']); + + // HTMX 요청이면 단계 콘텐츠만 반환 + if ($request->header('HX-Request') && $request->has('step')) { + return view('sales.modals.scenario-step', [ + 'prospect' => $prospect, + 'steps' => $steps, + 'currentStep' => $currentStep, + 'step' => collect($steps)->firstWhere('id', $currentStep), + 'progress' => $progress, + 'scenarioType' => 'sales', + 'icons' => $icons, + 'management' => $management, + 'isProspect' => true, + ]); + } + + return view('sales.modals.scenario-modal', [ + 'prospect' => $prospect, + 'steps' => $steps, + 'currentStep' => $currentStep, + 'progress' => $progress, + 'scenarioType' => 'sales', + 'icons' => $icons, + 'management' => $management, + 'isProspect' => true, + ]); + } + + /** + * 가망고객 매니저 시나리오 모달 뷰 + */ + public function prospectManagerScenario(int $prospectId, Request $request): View|Response + { + $prospect = TenantProspect::findOrFail($prospectId); + $steps = config('sales_scenario.manager_steps'); + $currentStep = (int) $request->input('step', 1); + $icons = config('sales_scenario.icons'); + + // 가망고객 영업 관리 정보 조회 또는 생성 + $management = SalesTenantManagement::findOrCreateByProspect($prospectId); + + // 체크리스트 진행 상태 조회 (DB에서) + $progress = SalesScenarioChecklist::calculateProgressByProspect($prospectId, 'manager', $steps); + + // 진행률 업데이트 + $management->updateProgress('manager', $progress['percentage']); + + // HTMX 요청이면 단계 콘텐츠만 반환 + if ($request->header('HX-Request') && $request->has('step')) { + return view('sales.modals.scenario-step', [ + 'prospect' => $prospect, + 'steps' => $steps, + 'currentStep' => $currentStep, + 'step' => collect($steps)->firstWhere('id', $currentStep), + 'progress' => $progress, + 'scenarioType' => 'manager', + 'icons' => $icons, + 'management' => $management, + 'isProspect' => true, + ]); + } + + return view('sales.modals.scenario-modal', [ + 'prospect' => $prospect, + 'steps' => $steps, + 'currentStep' => $currentStep, + 'progress' => $progress, + 'scenarioType' => 'manager', + 'icons' => $icons, + 'management' => $management, + 'isProspect' => true, + ]); + } + + /** + * 가망고객 체크리스트 항목 토글 (HTMX) + */ + public function toggleProspectChecklist(Request $request): JsonResponse + { + $request->validate([ + 'prospect_id' => 'required|integer|exists:tenant_prospects,id', + 'scenario_type' => 'required|in:sales,manager', + 'step_id' => 'required|integer', + 'checkpoint_id' => 'required|string', + 'checked' => 'required|boolean', + ]); + + $prospectId = $request->input('prospect_id'); + $scenarioType = $request->input('scenario_type'); + $stepId = $request->input('step_id'); + $checkpointId = $request->input('checkpoint_id'); + $checked = $request->boolean('checked'); + + // 체크리스트 토글 (DB에 저장) + SalesScenarioChecklist::toggleByProspect( + $prospectId, + $scenarioType, + $stepId, + $checkpointId, + $checked, + auth()->id() + ); + + // 진행률 재계산 + $steps = config("sales_scenario.{$scenarioType}_steps"); + $progress = SalesScenarioChecklist::calculateProgressByProspect($prospectId, $scenarioType, $steps); + + // 영업 관리 정보 업데이트 + $management = SalesTenantManagement::findOrCreateByProspect($prospectId); + $management->updateProgress($scenarioType, $progress['percentage']); + + return response()->json([ + 'success' => true, + 'progress' => $progress, + 'checked' => $checked, + ]); + } + + /** + * 가망고객 진행률 조회 + */ + public function getProspectProgress(int $prospectId, string $type): JsonResponse + { + $steps = config("sales_scenario.{$type}_steps"); + $progress = SalesScenarioChecklist::calculateProgressByProspect($prospectId, $type, $steps); + + return response()->json([ + 'success' => true, + 'progress' => $progress, + ]); + } } diff --git a/app/Models/Sales/SalesScenarioChecklist.php b/app/Models/Sales/SalesScenarioChecklist.php index 65d261a2..2e4264ce 100644 --- a/app/Models/Sales/SalesScenarioChecklist.php +++ b/app/Models/Sales/SalesScenarioChecklist.php @@ -27,6 +27,7 @@ class SalesScenarioChecklist extends Model protected $fillable = [ 'tenant_id', + 'tenant_prospect_id', 'scenario_type', 'step_id', 'checkpoint_id', @@ -53,6 +54,14 @@ public function tenant(): BelongsTo return $this->belongsTo(Tenant::class); } + /** + * 가망고객 관계 + */ + public function tenantProspect(): BelongsTo + { + return $this->belongsTo(TenantProspect::class, 'tenant_prospect_id'); + } + /** * 체크한 사용자 관계 */ @@ -70,7 +79,7 @@ public function user(): BelongsTo } /** - * 체크포인트 토글 + * 체크포인트 토글 (tenant_id 기반) */ public static function toggle(int $tenantId, string $scenarioType, int $stepId, string $checkpointId, bool $checked, ?int $userId = null): self { @@ -97,6 +106,34 @@ public static function toggle(int $tenantId, string $scenarioType, int $stepId, return $checklist; } + /** + * 체크포인트 토글 (prospect_id 기반) + */ + public static function toggleByProspect(int $prospectId, string $scenarioType, int $stepId, string $checkpointId, bool $checked, ?int $userId = null): self + { + $currentUserId = $userId ?? auth()->id(); + + $checklist = self::firstOrNew([ + 'tenant_prospect_id' => $prospectId, + 'scenario_type' => $scenarioType, + 'step_id' => $stepId, + 'checkpoint_id' => $checkpointId, + ]); + + // 새 레코드인 경우 필수 필드 설정 + if (!$checklist->exists) { + $checklist->user_id = $currentUserId; + $checklist->checkpoint_index = 0; + } + + $checklist->is_checked = $checked; + $checklist->checked_at = $checked ? now() : null; + $checklist->checked_by = $checked ? $currentUserId : null; + $checklist->save(); + + return $checklist; + } + /** * 특정 테넌트/시나리오의 체크리스트 조회 */ @@ -119,6 +156,28 @@ public static function getChecklist(int $tenantId, string $scenarioType): array return $result; } + /** + * 특정 가망고객/시나리오의 체크리스트 조회 + */ + public static function getChecklistByProspect(int $prospectId, string $scenarioType): array + { + $items = self::where('tenant_prospect_id', $prospectId) + ->where('scenario_type', $scenarioType) + ->where('is_checked', true) + ->get(); + + $result = []; + foreach ($items as $item) { + $key = "{$item->step_id}_{$item->checkpoint_id}"; + $result[$key] = [ + 'checked_at' => $item->checked_at?->toDateTimeString(), + 'checked_by' => $item->checked_by, + ]; + } + + return $result; + } + /** * 진행률 계산 */ @@ -158,6 +217,45 @@ public static function calculateProgress(int $tenantId, string $scenarioType, ar ]; } + /** + * 가망고객 기반 진행률 계산 + */ + public static function calculateProgressByProspect(int $prospectId, string $scenarioType, array $steps): array + { + $checklist = self::getChecklistByProspect($prospectId, $scenarioType); + + $totalCheckpoints = 0; + $completedCheckpoints = 0; + $stepProgress = []; + + foreach ($steps as $step) { + $stepCompleted = 0; + $stepTotal = count($step['checkpoints'] ?? []); + $totalCheckpoints += $stepTotal; + + foreach ($step['checkpoints'] as $checkpoint) { + $key = "{$step['id']}_{$checkpoint['id']}"; + if (isset($checklist[$key])) { + $completedCheckpoints++; + $stepCompleted++; + } + } + + $stepProgress[$step['id']] = [ + 'total' => $stepTotal, + 'completed' => $stepCompleted, + 'percentage' => $stepTotal > 0 ? round(($stepCompleted / $stepTotal) * 100) : 0, + ]; + } + + return [ + 'total' => $totalCheckpoints, + 'completed' => $completedCheckpoints, + 'percentage' => $totalCheckpoints > 0 ? round(($completedCheckpoints / $totalCheckpoints) * 100) : 0, + 'steps' => $stepProgress, + ]; + } + /** * 시나리오 타입 스코프 */ @@ -231,4 +329,55 @@ public static function getTenantProgress(int $tenantId): array 'manager' => self::getSimpleProgress($tenantId, 'manager'), ]; } + + /** + * 가망고객별 간단한 진행률 계산 + */ + public static function getSimpleProgressByProspect(int $prospectId, string $scenarioType): array + { + $configKey = $scenarioType === 'sales' ? 'sales_scenario.sales_steps' : 'sales_scenario.manager_steps'; + $steps = config($configKey, []); + + $total = 0; + $validCheckpointKeys = []; + + foreach ($steps as $step) { + foreach ($step['checkpoints'] ?? [] as $checkpoint) { + $total++; + $validCheckpointKeys[] = "{$step['id']}_{$checkpoint['id']}"; + } + } + + $checkedItems = self::where('tenant_prospect_id', $prospectId) + ->where('scenario_type', $scenarioType) + ->where('is_checked', true) + ->get(); + + $completed = 0; + foreach ($checkedItems as $item) { + $key = "{$item->step_id}_{$item->checkpoint_id}"; + if (in_array($key, $validCheckpointKeys)) { + $completed++; + } + } + + $percentage = $total > 0 ? round(($completed / $total) * 100) : 0; + + return [ + 'completed' => $completed, + 'total' => $total, + 'percentage' => $percentage, + ]; + } + + /** + * 가망고객별 영업/매니저 진행률 한번에 조회 + */ + public static function getProspectProgress(int $prospectId): array + { + return [ + 'sales' => self::getSimpleProgressByProspect($prospectId, 'sales'), + 'manager' => self::getSimpleProgressByProspect($prospectId, 'manager'), + ]; + } } diff --git a/app/Models/Sales/SalesTenantManagement.php b/app/Models/Sales/SalesTenantManagement.php index 377c21dd..c8aceb52 100644 --- a/app/Models/Sales/SalesTenantManagement.php +++ b/app/Models/Sales/SalesTenantManagement.php @@ -41,6 +41,7 @@ class SalesTenantManagement extends Model protected $fillable = [ 'tenant_id', + 'tenant_prospect_id', 'sales_partner_id', 'manager_user_id', 'sales_scenario_step', @@ -180,6 +181,14 @@ public function tenant(): BelongsTo return $this->belongsTo(Tenant::class); } + /** + * 가망고객 관계 + */ + public function tenantProspect(): BelongsTo + { + return $this->belongsTo(TenantProspect::class, 'tenant_prospect_id'); + } + /** * 영업 담당자 관계 */ @@ -243,6 +252,35 @@ public static function findOrCreateByTenant(int $tenantId): self ); } + /** + * 가망고객 ID로 조회 또는 생성 + */ + public static function findOrCreateByProspect(int $prospectId): self + { + return self::firstOrCreate( + ['tenant_prospect_id' => $prospectId], + [ + 'status' => self::STATUS_PROSPECT, + 'sales_scenario_step' => 1, + 'manager_scenario_step' => 1, + ] + ); + } + + /** + * 테넌트 또는 가망고객 ID로 조회 또는 생성 + */ + public static function findOrCreateByTenantOrProspect(?int $tenantId, ?int $prospectId): self + { + if ($tenantId) { + return self::findOrCreateByTenant($tenantId); + } + if ($prospectId) { + return self::findOrCreateByProspect($prospectId); + } + throw new \InvalidArgumentException('tenant_id 또는 prospect_id 중 하나는 필수입니다.'); + } + /** * 진행률 업데이트 */ diff --git a/resources/views/sales/dashboard/partials/tenant-list.blade.php b/resources/views/sales/dashboard/partials/tenant-list.blade.php index 94cce5b6..a3be675d 100644 --- a/resources/views/sales/dashboard/partials/tenant-list.blade.php +++ b/resources/views/sales/dashboard/partials/tenant-list.blade.php @@ -9,6 +9,15 @@ swap: 'innerHTML' }); }, + openProspectScenarioModal(prospectId, type) { + const url = type === 'sales' + ? `/sales/scenarios/prospect/${prospectId}/sales` + : `/sales/scenarios/prospect/${prospectId}/manager`; + htmx.ajax('GET', url, { + target: '#scenario-modal-container', + swap: 'innerHTML' + }); + }, confirmDeleteTenant(tenantId, companyName) { if (confirm(`"${companyName}" 테넌트를 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다.`)) { fetch(`/api/admin/tenants/${tenantId}`, { @@ -63,32 +72,77 @@