feat:가망고객 단계에서 영업/매니저 시나리오 체크리스트 지원

- SalesTenantManagement, SalesScenarioChecklist에 tenant_prospect_id 지원 추가
- 가망고객 기반 시나리오 컨트롤러 메서드 추가
- 라우트 추가: /sales/scenarios/prospect/{id}/sales, manager
- 대시보드에서 가망고객 행에 영업/매니저 버튼 및 진행률 표시
- 시나리오 모달/스텝 뷰 prospect 모드 지원

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-01-31 19:34:49 +09:00
parent 83af5ea473
commit fcb3a65cf7
7 changed files with 510 additions and 64 deletions

View File

@@ -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,
]);
}
}

View File

@@ -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'),
];
}
}

View File

@@ -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 중 하나는 필수입니다.');
}
/**
* 진행률 업데이트
*/

View File

@@ -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(`&quot;${companyName}&quot; 테넌트를 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다.`)) {
fetch(`/api/admin/tenants/${tenantId}`, {
@@ -63,32 +72,77 @@
</div>
<div class="divide-y divide-gray-100 border border-orange-200 rounded-lg bg-orange-50/30">
@foreach($prospects as $prospect)
<div class="flex items-center gap-4 px-4 py-3 hover:bg-orange-50 transition-colors">
<!-- 업체명 정보 -->
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<div class="font-bold text-gray-900 truncate">{{ $prospect->company_name }}</div>
<span class="px-2 py-0.5 text-xs font-medium rounded-full {{ $prospect->status_color }}">
{{ $prospect->status_label }}
</span>
@php
$prospectProgress = \App\Models\Sales\SalesScenarioChecklist::getProspectProgress($prospect->id);
$prospectManagement = \App\Models\Sales\SalesTenantManagement::findOrCreateByProspect($prospect->id);
@endphp
<div class="prospect-row flex items-center gap-4 px-4 py-3 hover:bg-orange-50 transition-colors" data-prospect-id="{{ $prospect->id }}">
<!-- 업체명 정보 + 영업/매니저 버튼 -->
<div class="flex items-center gap-3 flex-shrink-0">
<div class="min-w-0">
<div class="flex items-center gap-2">
<div class="font-bold text-gray-900 truncate">{{ $prospect->company_name }}</div>
<span class="px-2 py-0.5 text-xs font-medium rounded-full {{ $prospect->status_color }}">
{{ $prospect->status_label }}
</span>
</div>
<div class="text-xs text-gray-500 mt-0.5">
@if($prospect->ceo_name)
<span>대표: {{ $prospect->ceo_name }}</span>
<span class="mx-1">|</span>
@endif
<span>{{ $prospect->business_number ?? '-' }}</span>
</div>
</div>
<div class="text-xs text-gray-500 mt-0.5">
@if($prospect->ceo_name)
<span>대표: {{ $prospect->ceo_name }}</span>
<span class="mx-1">|</span>
@endif
<span>{{ $prospect->business_number ?? '-' }}</span>
@if($prospect->contact_phone)
<span class="mx-1">|</span>
<span>{{ $prospect->contact_phone }}</span>
@endif
{{-- 영업/매니저 진행 버튼 --}}
<div class="flex-shrink-0 flex items-center gap-1">
<button
x-on:click="openProspectScenarioModal({{ $prospect->id }}, 'sales')"
class="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-blue-600 text-white hover:bg-blue-700 transition-colors"
title="영업 시나리오">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
</svg>
영업
</button>
<button
x-on:click="openProspectScenarioModal({{ $prospect->id }}, 'manager')"
class="inline-flex items-center gap-1 px-2 py-1 rounded text-xs font-medium bg-green-600 text-white hover:bg-green-700 transition-colors"
title="매니저 시나리오">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
매니저
</button>
</div>
</div>
<!-- 진행 현황 (영업/매니저 프로그레스 ) -->
<div class="flex-1 min-w-0" id="prospect-progress-{{ $prospect->id }}">
<div class="space-y-1.5">
{{-- 영업 --}}
<div class="flex items-center gap-2" title="영업 {{ $prospectProgress['sales']['percentage'] }}%">
<span class="text-xs font-medium text-blue-600 w-6 flex-shrink-0">영업</span>
<div class="flex-1 bg-gray-200 rounded-full h-2 min-w-0">
<div class="bg-blue-500 h-2 rounded-full transition-all" id="prospect-sales-bar-{{ $prospect->id }}" style="width: {{ $prospectProgress['sales']['percentage'] }}%"></div>
</div>
<span class="text-xs text-gray-500 w-10 text-right flex-shrink-0" id="prospect-sales-pct-{{ $prospect->id }}">{{ $prospectProgress['sales']['percentage'] }}%</span>
</div>
{{-- 매니저 --}}
<div class="flex items-center gap-2" title="매니저 {{ $prospectProgress['manager']['percentage'] }}%">
<span class="text-xs font-medium text-green-600 w-6 flex-shrink-0">매니</span>
<div class="flex-1 bg-gray-200 rounded-full h-2 min-w-0">
<div class="bg-green-500 h-2 rounded-full transition-all" id="prospect-manager-bar-{{ $prospect->id }}" style="width: {{ $prospectProgress['manager']['percentage'] }}%"></div>
</div>
<span class="text-xs text-gray-500 w-10 text-right flex-shrink-0" id="prospect-manager-pct-{{ $prospect->id }}">{{ $prospectProgress['manager']['percentage'] }}%</span>
</div>
</div>
</div>
<!-- 영업권 유효기간 -->
<div class="flex-shrink-0 text-center">
<div class="flex-shrink-0 text-center border-l border-gray-200 pl-4">
@if($prospect->isActive())
<div class="text-xs text-gray-400">영업권 만료까지</div>
<div class="text-xs text-gray-400">영업권 만료</div>
<div class="text-lg font-bold text-orange-600">D-{{ $prospect->remaining_days }}</div>
@elseif($prospect->isExpired())
<div class="text-xs text-gray-400">상태</div>
@@ -96,16 +150,10 @@
@endif
</div>
<!-- 등록일 -->
<div class="flex-shrink-0 text-right border-l border-gray-200 pl-4">
<div class="text-xs text-gray-400">등록일</div>
<div class="text-sm font-medium text-gray-700">{{ $prospect->registered_at?->format('Y-m-d') ?? $prospect->created_at->format('Y-m-d') }}</div>
</div>
<!-- 고객 관리 바로가기 -->
<div class="flex-shrink-0">
<div class="flex-shrink-0 border-l border-gray-200 pl-4">
<a href="{{ route('sales.prospects.index') }}?search={{ $prospect->business_number }}"
class="inline-flex items-center gap-1 px-3 py-1.5 rounded text-xs font-medium bg-orange-600 text-white hover:bg-orange-700 transition-colors">
class="inline-flex items-center gap-1 px-3 py-1.5 rounded text-xs font-medium bg-gray-600 text-white hover:bg-gray-700 transition-colors">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />

View File

@@ -1,6 +1,9 @@
{{-- 영업/매니저 시나리오 모달 --}}
@php
$stepProgressJson = json_encode($progress['steps'] ?? []);
$isProspectMode = isset($isProspect) && $isProspect;
$entity = $isProspectMode ? $prospect : $tenant;
$entityId = $entity->id;
@endphp
<div x-data="{
@@ -8,14 +11,16 @@
currentStep: {{ $currentStep }},
totalProgress: {{ $progress['percentage'] ?? 0 }},
stepProgress: {{ $stepProgressJson }},
tenantId: {{ $tenant->id }},
entityId: {{ $entityId }},
isProspect: {{ $isProspectMode ? 'true' : 'false' }},
scenarioType: '{{ $scenarioType }}',
close() {
this.isOpen = false;
window.dispatchEvent(new CustomEvent('scenario-modal-closed', {
detail: {
tenantId: this.tenantId,
tenantId: this.isProspect ? null : this.entityId,
prospectId: this.isProspect ? this.entityId : null,
scenarioType: this.scenarioType,
progress: this.totalProgress
}
@@ -29,7 +34,8 @@
// 모달 닫힘 이벤트 발송
window.dispatchEvent(new CustomEvent('scenario-modal-closed', {
detail: {
tenantId: detail.tenantId,
tenantId: this.isProspect ? null : this.entityId,
prospectId: this.isProspect ? this.entityId : null,
scenarioType: detail.scenarioType,
progress: this.totalProgress,
completed: true
@@ -40,28 +46,41 @@
selectStep(stepId) {
if (this.currentStep === stepId) return;
this.currentStep = stepId;
htmx.ajax('GET',
`/sales/scenarios/${this.tenantId}/${this.scenarioType}?step=${stepId}`,
{ target: '#scenario-step-content', swap: 'innerHTML' }
);
const baseUrl = this.isProspect
? `/sales/scenarios/prospect/${this.entityId}/${this.scenarioType}`
: `/sales/scenarios/${this.entityId}/${this.scenarioType}`;
htmx.ajax('GET', `${baseUrl}?step=${stepId}`, { target: '#scenario-step-content', swap: 'innerHTML' });
},
async toggleCheckpoint(stepId, checkpointId, checked) {
try {
const response = await fetch('/sales/scenarios/checklist/toggle', {
const url = this.isProspect
? '/sales/scenarios/prospect/checklist/toggle'
: '/sales/scenarios/checklist/toggle';
const bodyData = this.isProspect
? {
prospect_id: this.entityId,
scenario_type: this.scenarioType,
step_id: stepId,
checkpoint_id: checkpointId,
checked: checked,
}
: {
tenant_id: this.entityId,
scenario_type: this.scenarioType,
step_id: stepId,
checkpoint_id: checkpointId,
checked: checked,
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
'Accept': 'application/json',
},
body: JSON.stringify({
tenant_id: this.tenantId,
scenario_type: this.scenarioType,
step_id: stepId,
checkpoint_id: checkpointId,
checked: checked,
}),
body: JSON.stringify(bodyData),
});
const result = await response.json();
if (result.success) {
@@ -119,7 +138,7 @@ class="fixed inset-0 z-50 overflow-hidden"
<h2 class="text-xl font-bold text-white">
{{ $scenarioType === 'sales' ? '영업 전략 시나리오' : '매니저 상담 프로세스' }}
</h2>
<p class="text-sm text-white/80">{{ $tenant->company_name }}</p>
<p class="text-sm text-white/80">{{ $entity->company_name }}</p>
</div>
</div>
<div class="flex items-center gap-4">
@@ -189,7 +208,8 @@ class="w-full flex items-center gap-3 px-3 py-3 rounded-r-lg text-left transitio
</div>
</div>
{{-- 하단 고정: 상담 기록 첨부파일 (모든 단계 공유) --}}
{{-- 하단 고정: 상담 기록 첨부파일 (테넌트 전용, 가망고객은 미지원) --}}
@if(!$isProspectMode)
<div x-data="{ consultationExpanded: false }" class="flex-shrink-0 border-t border-gray-200 bg-gray-50">
{{-- 아코디언 헤더 --}}
<button type="button"
@@ -229,7 +249,7 @@ class="overflow-y-auto bg-white border-t border-gray-200"
<div class="p-6 space-y-4">
{{-- 상담 기록 --}}
<div id="consultation-log-container"
hx-get="{{ route('sales.consultations.index', $tenant->id) }}?scenario_type={{ $scenarioType }}"
hx-get="{{ route('sales.consultations.index', $entity->id) }}?scenario_type={{ $scenarioType }}"
hx-trigger="revealed"
hx-swap="innerHTML">
<div class="animate-pulse flex space-x-4">
@@ -246,7 +266,7 @@ class="overflow-y-auto bg-white border-t border-gray-200"
{{-- 음성 녹음 --}}
<div>
@include('sales.modals.voice-recorder', [
'tenant' => $tenant,
'tenant' => $entity,
'scenarioType' => $scenarioType,
'stepId' => null,
])
@@ -255,7 +275,7 @@ class="overflow-y-auto bg-white border-t border-gray-200"
{{-- 첨부파일 업로드 --}}
<div>
@include('sales.modals.file-uploader', [
'tenant' => $tenant,
'tenant' => $entity,
'scenarioType' => $scenarioType,
'stepId' => null,
])
@@ -263,6 +283,7 @@ class="overflow-y-auto bg-white border-t border-gray-200"
</div>
</div>
</div>
@endif
</div>
</div>
</div>

View File

@@ -7,8 +7,16 @@
$steps = config($scenarioType === 'sales' ? 'sales_scenario.sales_steps' : 'sales_scenario.manager_steps', []);
}
$step = $step ?? collect($steps)->firstWhere('id', $currentStep ?? 1);
// prospect 모드 확인
$isProspectMode = isset($isProspect) && $isProspect;
$entity = $isProspectMode ? $prospect : $tenant;
$entityId = $entity->id;
// DB에서 체크된 항목 조회
$checklist = SalesScenarioChecklist::getChecklist($tenant->id, $scenarioType);
$checklist = $isProspectMode
? SalesScenarioChecklist::getChecklistByProspect($entityId, $scenarioType)
: SalesScenarioChecklist::getChecklist($entityId, $scenarioType);
@endphp
<div class="space-y-6">
@@ -54,23 +62,38 @@
<div x-data="{
expanded: false,
checked: {{ $isChecked ? 'true' : 'false' }},
isProspect: {{ $isProspectMode ? 'true' : 'false' }},
entityId: {{ $entityId }},
async toggle() {
this.checked = !this.checked;
try {
const response = await fetch('/sales/scenarios/checklist/toggle', {
const url = this.isProspect
? '/sales/scenarios/prospect/checklist/toggle'
: '/sales/scenarios/checklist/toggle';
const bodyData = this.isProspect
? {
prospect_id: this.entityId,
scenario_type: '{{ $scenarioType }}',
step_id: {{ $step['id'] }},
checkpoint_id: '{{ $checkpoint['id'] }}',
checked: this.checked,
}
: {
tenant_id: this.entityId,
scenario_type: '{{ $scenarioType }}',
step_id: {{ $step['id'] }},
checkpoint_id: '{{ $checkpoint['id'] }}',
checked: this.checked,
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content,
'Accept': 'application/json',
},
body: JSON.stringify({
tenant_id: {{ $tenant->id }},
scenario_type: '{{ $scenarioType }}',
step_id: {{ $step['id'] }},
checkpoint_id: '{{ $checkpoint['id'] }}',
checked: this.checked,
}),
body: JSON.stringify(bodyData),
});
const result = await response.json();
if (result.success) {
@@ -149,9 +172,9 @@ class="border-t border-gray-100">
@endforeach
</div>
{{-- 계약 체결 단계 (Step 6)에서만 상품 선택 컴포넌트 표시 --}}
@if($step['id'] === 6 && $scenarioType === 'sales')
@include('sales.modals.partials.product-selection', ['tenant' => $tenant])
{{-- 계약 체결 단계 (Step 6)에서만 상품 선택 컴포넌트 표시 (테넌트 전용) --}}
@if($step['id'] === 6 && $scenarioType === 'sales' && !$isProspectMode)
@include('sales.modals.partials.product-selection', ['tenant' => $entity])
@endif
{{-- 단계 이동 버튼 --}}
@@ -162,12 +185,17 @@ class="border-t border-gray-100">
$nextStepId = $currentStepId + 1;
$prevStepId = $currentStepId - 1;
$stepColor = $step['color'] ?? 'blue';
// 라우트 결정
$routeName = $isProspectMode
? 'sales.scenarios.prospect.' . $scenarioType
: 'sales.scenarios.' . $scenarioType;
@endphp
<div class="flex items-center justify-between pt-4 border-t border-gray-200">
{{-- 이전 단계 버튼 --}}
@if($currentStepId > 1)
<button type="button"
hx-get="{{ route('sales.scenarios.' . $scenarioType, $tenant->id) }}?step={{ $prevStepId }}"
hx-get="{{ route($routeName, $entityId) }}?step={{ $prevStepId }}"
hx-target="#scenario-step-content"
hx-swap="innerHTML"
x-on:click="window.dispatchEvent(new CustomEvent('step-changed', { detail: {{ $prevStepId }} }))"
@@ -184,7 +212,7 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-70
{{-- 다음 단계 / 완료 버튼 --}}
@if($isLastStep)
<button type="button"
x-on:click="window.dispatchEvent(new CustomEvent('scenario-completed', { detail: { tenantId: {{ $tenant->id }}, scenarioType: '{{ $scenarioType }}' } }))"
x-on:click="window.dispatchEvent(new CustomEvent('scenario-completed', { detail: { {{ $isProspectMode ? 'prospectId' : 'tenantId' }}: {{ $entityId }}, scenarioType: '{{ $scenarioType }}' } }))"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
@@ -193,7 +221,7 @@ class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white b
</button>
@else
<button type="button"
hx-get="{{ route('sales.scenarios.' . $scenarioType, $tenant->id) }}?step={{ $nextStepId }}"
hx-get="{{ route($routeName, $entityId) }}?step={{ $nextStepId }}"
hx-target="#scenario-step-content"
hx-swap="innerHTML"
x-on:click="window.dispatchEvent(new CustomEvent('step-changed', { detail: {{ $nextStepId }} }))"

View File

@@ -909,10 +909,17 @@
// 영업 시나리오 관리
Route::prefix('scenarios')->name('scenarios.')->group(function () {
// 테넌트 기반 (기존)
Route::get('/{tenant}/sales', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'salesScenario'])->name('sales');
Route::get('/{tenant}/manager', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'managerScenario'])->name('manager');
Route::post('/checklist/toggle', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'toggleChecklist'])->name('checklist.toggle');
Route::get('/{tenant}/{type}/progress', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'getProgress'])->name('progress');
// 가망고객 기반 (신규)
Route::get('/prospect/{prospect}/sales', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'prospectSalesScenario'])->name('prospect.sales');
Route::get('/prospect/{prospect}/manager', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'prospectManagerScenario'])->name('prospect.manager');
Route::post('/prospect/checklist/toggle', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'toggleProspectChecklist'])->name('prospect.checklist.toggle');
Route::get('/prospect/{prospect}/{type}/progress', [\App\Http\Controllers\Sales\SalesScenarioController::class, 'getProspectProgress'])->name('prospect.progress');
});
// 상담 기록 관리