- 인계 완료 섹션에 영업/매니저 기록 조회 버튼 추가 - readonly 모드로 열어 수정 불가, 조회만 가능 - prospectManagerScenario에 readonly 파라미터 지원 추가 - 단계 이동 시 readonly 파라미터 유지 - 마지막 단계 버튼 텍스트 조건부 표시 (완료/닫기) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
252 lines
13 KiB
PHP
252 lines
13 KiB
PHP
{{-- 시나리오 단계별 체크리스트 --}}
|
|
@php
|
|
use App\Models\Sales\SalesScenarioChecklist;
|
|
|
|
// $steps가 없거나 비어있으면 config에서 가져오기 (안전장치)
|
|
if (empty($steps)) {
|
|
$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;
|
|
|
|
// 읽기 전용 모드 확인
|
|
$isReadonly = isset($readonly) && $readonly;
|
|
|
|
// DB에서 체크된 항목 조회
|
|
$checklist = $isProspectMode
|
|
? SalesScenarioChecklist::getChecklistByProspect($entityId, $scenarioType)
|
|
: SalesScenarioChecklist::getChecklist($entityId, $scenarioType);
|
|
@endphp
|
|
|
|
<div class="space-y-6">
|
|
{{-- 단계 헤더 --}}
|
|
<div class="flex items-start gap-4">
|
|
<div class="{{ $step['bg_class'] }} {{ $step['text_class'] }} p-3 rounded-xl">
|
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
{!! $icons[$step['icon']] ?? '' !!}
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm font-medium text-gray-500">STEP {{ $step['id'] }}</span>
|
|
<span class="text-sm text-gray-400">{{ $step['subtitle'] }}</span>
|
|
</div>
|
|
<h2 class="text-2xl font-bold text-gray-900">{{ $step['title'] }}</h2>
|
|
<p class="mt-1 text-gray-600">{{ $step['description'] }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- 매니저용 팁 (있는 경우) --}}
|
|
@if(isset($step['tips']))
|
|
<div class="bg-amber-50 border border-amber-200 rounded-lg p-4">
|
|
<div class="flex items-start gap-3">
|
|
<svg class="w-5 h-5 text-amber-600 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
|
</svg>
|
|
<div>
|
|
<p class="text-sm font-medium text-amber-800">매니저 TIP</p>
|
|
<p class="text-sm text-amber-700">{{ $step['tips'] }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
{{-- 체크포인트 목록 --}}
|
|
<div class="space-y-4">
|
|
@foreach($step['checkpoints'] as $checkpoint)
|
|
@php
|
|
$checkKey = "{$step['id']}_{$checkpoint['id']}";
|
|
$isChecked = isset($checklist[$checkKey]);
|
|
@endphp
|
|
<div x-data="{
|
|
expanded: false,
|
|
checked: {{ $isChecked ? 'true' : 'false' }},
|
|
isProspect: {{ $isProspectMode ? 'true' : 'false' }},
|
|
entityId: {{ $entityId }},
|
|
readonly: {{ $isReadonly ? 'true' : 'false' }},
|
|
async toggle() {
|
|
if (this.readonly) return;
|
|
this.checked = !this.checked;
|
|
try {
|
|
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(bodyData),
|
|
});
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
window.dispatchEvent(new CustomEvent('progress-updated', { detail: result.progress }));
|
|
}
|
|
} catch (error) {
|
|
console.error('체크리스트 토글 실패:', error);
|
|
this.checked = !this.checked;
|
|
}
|
|
}
|
|
}"
|
|
class="bg-white border rounded-xl overflow-hidden transition-all duration-200"
|
|
:class="checked ? 'border-green-300 bg-green-50/50' : 'border-gray-200 hover:border-gray-300'">
|
|
|
|
{{-- 체크포인트 헤더 --}}
|
|
<div class="flex items-center gap-4 p-4 cursor-pointer" @click="expanded = !expanded">
|
|
{{-- 체크박스 --}}
|
|
<button type="button"
|
|
@click.stop="toggle()"
|
|
class="flex-shrink-0 w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all"
|
|
:class="[
|
|
checked ? 'bg-green-500 border-green-500' : 'border-gray-300',
|
|
readonly ? 'cursor-not-allowed opacity-60' : 'hover:border-green-400'
|
|
]"
|
|
:disabled="readonly">
|
|
<svg x-show="checked" class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{{-- 제목 및 설명 --}}
|
|
<div class="flex-1 min-w-0">
|
|
<h4 class="font-semibold text-gray-900" :class="checked && 'line-through text-gray-500'">
|
|
{{ $checkpoint['title'] }}
|
|
</h4>
|
|
<p class="text-sm text-gray-600 truncate">{{ $checkpoint['detail'] }}</p>
|
|
</div>
|
|
|
|
{{-- 확장 아이콘 --}}
|
|
<svg class="w-5 h-5 text-gray-400 transition-transform duration-200"
|
|
:class="expanded && '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>
|
|
|
|
{{-- 확장 콘텐츠 --}}
|
|
<div x-show="expanded"
|
|
x-transition:enter="transition ease-out duration-200"
|
|
x-transition:enter-start="opacity-0 -translate-y-2"
|
|
x-transition:enter-end="opacity-100 translate-y-0"
|
|
x-transition:leave="transition ease-in duration-150"
|
|
x-transition:leave-start="opacity-100 translate-y-0"
|
|
x-transition:leave-end="opacity-0 -translate-y-2"
|
|
class="border-t border-gray-100">
|
|
<div class="p-4 space-y-4">
|
|
{{-- 상세 설명 --}}
|
|
<div>
|
|
<h5 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-2">상세 설명</h5>
|
|
<p class="text-sm text-gray-700">{{ $checkpoint['detail'] }}</p>
|
|
</div>
|
|
|
|
{{-- PRO TIP --}}
|
|
<div class="bg-gradient-to-r from-indigo-50 to-purple-50 rounded-lg p-4">
|
|
<div class="flex items-start gap-3">
|
|
<div class="p-1.5 bg-indigo-100 rounded-lg">
|
|
<svg class="w-4 h-4 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-indigo-800 uppercase">PRO TIP</p>
|
|
<p class="text-sm text-indigo-700 mt-1">{{ $checkpoint['pro_tip'] }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
{{-- 계약 체결 단계 (Step 6)에서만 상품 선택 컴포넌트 표시 --}}
|
|
@if($step['id'] === 6 && $scenarioType === 'sales')
|
|
@include('sales.modals.partials.product-selection', [
|
|
'entity' => $entity,
|
|
'isProspect' => $isProspectMode,
|
|
])
|
|
@endif
|
|
|
|
{{-- 단계 이동 버튼 --}}
|
|
@php
|
|
$currentStepId = (int) $step['id'];
|
|
$totalSteps = count($steps);
|
|
$isLastStep = ($currentStepId >= $totalSteps);
|
|
$nextStepId = $currentStepId + 1;
|
|
$prevStepId = $currentStepId - 1;
|
|
$stepColor = $step['color'] ?? 'blue';
|
|
|
|
// 라우트 결정
|
|
$routeName = $isProspectMode
|
|
? 'sales.scenarios.prospect.' . $scenarioType
|
|
: 'sales.scenarios.' . $scenarioType;
|
|
|
|
// readonly 파라미터
|
|
$readonlyParam = $isReadonly ? '&readonly=1' : '';
|
|
@endphp
|
|
<div class="flex items-center justify-between pt-4 border-t border-gray-200">
|
|
{{-- 이전 단계 버튼 --}}
|
|
@if($currentStepId > 1)
|
|
<button type="button"
|
|
hx-get="{{ route($routeName, $entityId) }}?step={{ $prevStepId }}{{ $readonlyParam }}"
|
|
hx-target="#scenario-step-content"
|
|
hx-swap="innerHTML"
|
|
x-on:click="window.dispatchEvent(new CustomEvent('step-changed', { detail: {{ $prevStepId }} }))"
|
|
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 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="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
이전 단계
|
|
</button>
|
|
@else
|
|
<div></div>
|
|
@endif
|
|
|
|
{{-- 다음 단계 / 완료 버튼 --}}
|
|
@if($isLastStep)
|
|
<button type="button"
|
|
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" />
|
|
</svg>
|
|
{{ $isReadonly ? '닫기' : '완료' }}
|
|
</button>
|
|
@else
|
|
<button type="button"
|
|
hx-get="{{ route($routeName, $entityId) }}?step={{ $nextStepId }}{{ $readonlyParam }}"
|
|
hx-target="#scenario-step-content"
|
|
hx-swap="innerHTML"
|
|
x-on:click="window.dispatchEvent(new CustomEvent('step-changed', { detail: {{ $nextStepId }} }))"
|
|
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-lg hover:bg-indigo-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="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
@endif
|
|
</div>
|
|
</div>
|