- 내 활동 탭에 "매니저로 참여 중인 건" 섹션 추가 - 영업 시나리오: 읽기 전용 모드(참조용) 지원 - 매니저 시나리오: 체크 가능 - 시나리오 모달에 readonly 파라미터 처리 - 읽기 전용 시 체크박스 비활성화 및 "참조용" 배지 표시 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
310 lines
17 KiB
PHP
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>
|