Files
sam-manage/resources/views/sales/modals/scenario-modal.blade.php
김보곤 d472d10439 feat:매니저 대시보드에 매니저 참여 건 섹션 추가
- 내 활동 탭에 "매니저로 참여 중인 건" 섹션 추가
- 영업 시나리오: 읽기 전용 모드(참조용) 지원
- 매니저 시나리오: 체크 가능
- 시나리오 모달에 readonly 파라미터 처리
- 읽기 전용 시 체크박스 비활성화 및 "참조용" 배지 표시

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 15:03:00 +09:00

310 lines
17 KiB
PHP

{{-- 영업/매니저 시나리오 모달 --}}
@php
$stepProgressJson = json_encode($progress['steps'] ?? []);
$isProspectMode = isset($isProspect) && $isProspect;
$entity = $isProspectMode ? $prospect : $tenant;
$entityId = $entity->id;
$isReadonly = isset($readonly) && $readonly;
@endphp
<div x-data="{
isOpen: true,
currentStep: {{ $currentStep }},
totalProgress: {{ $progress['percentage'] ?? 0 }},
stepProgress: {{ $stepProgressJson }},
entityId: {{ $entityId }},
isProspect: {{ $isProspectMode ? 'true' : 'false' }},
scenarioType: '{{ $scenarioType }}',
readonly: {{ $isReadonly ? 'true' : 'false' }},
close() {
this.isOpen = false;
window.dispatchEvent(new CustomEvent('scenario-modal-closed', {
detail: {
tenantId: this.isProspect ? null : this.entityId,
prospectId: this.isProspect ? this.entityId : null,
scenarioType: this.scenarioType,
progress: this.totalProgress
}
}));
},
completeAndRefresh(detail) {
this.isOpen = false;
// 테넌트 리스트 새로고침 (진행률 반영)
htmx.ajax('GET', '{{ route("sales.salesmanagement.dashboard.tenants") }}', { target: '#tenant-list-container', swap: 'innerHTML' });
// 모달 닫힘 이벤트 발송
window.dispatchEvent(new CustomEvent('scenario-modal-closed', {
detail: {
tenantId: this.isProspect ? null : this.entityId,
prospectId: this.isProspect ? this.entityId : null,
scenarioType: detail.scenarioType,
progress: this.totalProgress,
completed: true
}
}));
},
selectStep(stepId) {
if (this.currentStep === stepId) return;
this.currentStep = stepId;
const baseUrl = this.isProspect
? `/sales/scenarios/prospect/${this.entityId}/${this.scenarioType}`
: `/sales/scenarios/${this.entityId}/${this.scenarioType}`;
const readonlyParam = this.readonly ? '&readonly=1' : '';
htmx.ajax('GET', `${baseUrl}?step=${stepId}${readonlyParam}`, { target: '#scenario-step-content', swap: 'innerHTML' });
},
async toggleCheckpoint(stepId, checkpointId, checked) {
// 읽기 전용 모드에서는 체크 불가
if (this.readonly) {
return;
}
try {
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(bodyData),
});
const result = await response.json();
if (result.success) {
this.totalProgress = result.progress.percentage;
this.stepProgress = result.progress.steps;
}
} catch (error) {
console.error('체크리스트 토글 실패:', error);
}
}
}"
x-show="isOpen"
x-cloak
@keydown.escape.window="close()"
@progress-updated.window="totalProgress = $event.detail.percentage; stepProgress = $event.detail.steps"
@step-changed.window="currentStep = $event.detail"
@scenario-completed.window="completeAndRefresh($event.detail)"
class="fixed inset-0 z-50 overflow-hidden"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
{{-- 배경 오버레이 --}}
<div class="absolute inset-0 bg-gray-900/50 backdrop-blur-sm" @click="close()"></div>
{{-- 모달 컨테이너 --}}
<div class="relative flex items-center justify-center min-h-screen p-4">
<div class="relative bg-white rounded-2xl shadow-2xl w-full max-w-6xl max-h-[90vh] overflow-hidden flex flex-col"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
@click.stop>
{{-- 모달 헤더 --}}
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200 {{ $scenarioType === 'sales' ? 'bg-blue-600' : 'bg-green-600' }}">
<div class="flex items-center gap-4">
<div class="p-2 bg-white/20 rounded-lg">
@if($scenarioType === 'sales')
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
@else
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
@endif
</div>
<div>
<div class="flex items-center gap-2">
<h2 class="text-xl font-bold text-white">
{{ $scenarioType === 'sales' ? '영업 전략 시나리오' : '매니저 상담 프로세스' }}
</h2>
@if($isReadonly)
<span class="px-2 py-0.5 bg-white/30 text-white text-xs font-medium rounded-full">참조용</span>
@endif
</div>
<p class="text-sm text-white/80">{{ $entity->company_name }}</p>
</div>
</div>
<div class="flex items-center gap-4">
{{-- 전체 진행률 --}}
<div class="flex items-center gap-2 bg-white/20 rounded-full px-4 py-2">
<span class="text-sm font-medium text-white">진행률</span>
<span class="text-lg font-bold text-white" x-text="totalProgress + '%'"></span>
</div>
{{-- 닫기 버튼 --}}
<button type="button" @click="close()" class="p-2 text-white/80 hover:text-white hover:bg-white/20 rounded-lg transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{{-- 모달 바디 --}}
<div class="flex flex-1 overflow-hidden">
{{-- 좌측 사이드바: 단계 네비게이션 --}}
<div class="w-64 bg-gray-50 border-r border-gray-200 overflow-y-auto">
<div class="p-4">
<h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4">단계별 진행</h3>
<nav class="space-y-2">
@foreach($steps as $step)
<button type="button"
@click="selectStep({{ $step['id'] }})"
:class="currentStep === {{ $step['id'] }}
? 'bg-white shadow-sm border-l-4 border-{{ $step['color'] }}-500'
: 'hover:bg-white/50 border-l-4 border-transparent'"
class="w-full flex items-center gap-3 px-3 py-3 rounded-r-lg text-left transition-all">
<div class="{{ $step['bg_class'] }} {{ $step['text_class'] }} p-2 rounded-lg">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{!! $icons[$step['icon']] ?? '' !!}
</svg>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<span class="text-sm font-medium text-gray-900 truncate">{{ $step['title'] }}</span>
<span class="text-xs text-gray-500"
x-text="(stepProgress[{{ $step['id'] }}]?.percentage ?? 0) + '%'"></span>
</div>
<div class="mt-1 h-1 bg-gray-200 rounded-full overflow-hidden">
<div class="h-full bg-{{ $step['color'] }}-500 transition-all duration-300"
:style="'width: ' + (stepProgress[{{ $step['id'] }}]?.percentage ?? 0) + '%'"></div>
</div>
</div>
</button>
@endforeach
</nav>
</div>
</div>
{{-- 우측 메인 영역 --}}
<div class="flex-1 flex flex-col min-h-0">
{{-- 단계별 콘텐츠 (스크롤 가능) --}}
<div class="flex-1 min-h-0 overflow-y-auto">
<div id="scenario-step-content" class="p-6">
@include('sales.modals.scenario-step', [
'step' => collect($steps)->firstWhere('id', $currentStep),
'steps' => $steps,
'tenant' => $isProspectMode ? null : $entity,
'prospect' => $isProspectMode ? $entity : null,
'isProspect' => $isProspectMode,
'scenarioType' => $scenarioType,
'progress' => $progress,
'icons' => $icons,
])
</div>
</div>
{{-- 하단 고정: 상담 기록 첨부파일 --}}
<div x-data="{ consultationExpanded: false }" class="flex-shrink-0 border-t border-gray-200 bg-gray-50">
{{-- 아코디언 헤더 --}}
<button type="button"
@click="consultationExpanded = !consultationExpanded"
class="w-full flex items-center justify-between px-6 py-3 hover:bg-gray-100 transition-colors">
<div class="flex items-center gap-3">
<div class="p-2 bg-purple-100 rounded-lg">
<svg class="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
</div>
<div class="text-left">
<h3 class="text-sm font-semibold text-gray-900">상담 기록 첨부파일</h3>
<p class="text-xs text-gray-500">음성 녹음, 메모, 파일 첨부 (모든 단계 공유)</p>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-xs text-gray-400" x-text="consultationExpanded ? '접기' : '펼치기'"></span>
<svg class="w-5 h-5 text-gray-400 transition-transform duration-200"
:class="consultationExpanded && 'rotate-180'"
fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
{{-- 아코디언 콘텐츠 --}}
<div x-show="consultationExpanded"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
class="overflow-y-auto bg-white border-t border-gray-200"
style="max-height: 50vh;">
<div class="p-6 space-y-4">
{{-- 상담 기록 --}}
<div id="consultation-log-container"
@if($isProspectMode)
hx-get="{{ route('sales.consultations.prospect.index', $entity->id) }}?scenario_type={{ $scenarioType }}"
@else
hx-get="{{ route('sales.consultations.index', $entity->id) }}?scenario_type={{ $scenarioType }}"
@endif
hx-trigger="revealed"
hx-swap="innerHTML">
<div class="animate-pulse flex space-x-4">
<div class="flex-1 space-y-4 py-1">
<div class="h-4 bg-gray-200 rounded w-3/4"></div>
<div class="space-y-2">
<div class="h-4 bg-gray-200 rounded"></div>
<div class="h-4 bg-gray-200 rounded w-5/6"></div>
</div>
</div>
</div>
</div>
{{-- 음성 녹음 --}}
<div>
@include('sales.modals.voice-recorder', [
'entity' => $entity,
'isProspect' => $isProspectMode,
'scenarioType' => $scenarioType,
'stepId' => null,
])
</div>
{{-- 첨부파일 업로드 --}}
<div>
@include('sales.modals.file-uploader', [
'entity' => $entity,
'isProspect' => $isProspectMode,
'scenarioType' => $scenarioType,
'stepId' => null,
])
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>