feat:영업파트너 고객관리 메뉴 추가 (관리자 전용)
- AdminProspectController 생성 (관리자/슈퍼관리자만 접근) - 전체 영업파트너의 고객 현황을 한눈에 파악 - 영업파트너별 필터, 상태별 필터 제공 - 영업/매니저 진행률 및 개발 상태 표시 - 상세 모달에서 담당자 정보 및 진행 현황 확인 - AdminProspectMenuSeeder 생성 (메뉴 추가용) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
133
app/Http/Controllers/Sales/AdminProspectController.php
Normal file
133
app/Http/Controllers/Sales/AdminProspectController.php
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Sales;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Sales\SalesScenarioChecklist;
|
||||||
|
use App\Models\Sales\SalesTenantManagement;
|
||||||
|
use App\Models\Sales\TenantProspect;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 관리자용 전체 영업파트너 고객 관리 컨트롤러
|
||||||
|
* 관리자/슈퍼관리자만 접근 가능
|
||||||
|
*/
|
||||||
|
class AdminProspectController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
// 관리자 권한 체크 미들웨어
|
||||||
|
$this->middleware(function ($request, $next) {
|
||||||
|
if (!auth()->user()->isAdmin() && !auth()->user()->isSuperAdmin()) {
|
||||||
|
abort(403, '관리자만 접근할 수 있습니다.');
|
||||||
|
}
|
||||||
|
return $next($request);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 고객 목록 페이지
|
||||||
|
*/
|
||||||
|
public function index(Request $request): View|Response
|
||||||
|
{
|
||||||
|
if ($request->header('HX-Request')) {
|
||||||
|
return response('', 200)->header('HX-Redirect', route('sales.admin-prospects.index'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 영업 역할을 가진 사용자 목록 (영업파트너)
|
||||||
|
$salesPartners = User::whereHas('userRoles', function ($q) {
|
||||||
|
$q->whereHas('role', function ($rq) {
|
||||||
|
$rq->whereIn('name', ['sales', 'manager', 'recruiter']);
|
||||||
|
});
|
||||||
|
})->orderBy('name')->get();
|
||||||
|
|
||||||
|
// 필터
|
||||||
|
$filters = [
|
||||||
|
'search' => $request->get('search'),
|
||||||
|
'status' => $request->get('status'),
|
||||||
|
'registered_by' => $request->get('registered_by'), // 특정 영업파트너 필터
|
||||||
|
];
|
||||||
|
|
||||||
|
// 쿼리 빌드
|
||||||
|
$query = TenantProspect::with(['registeredBy', 'tenant']);
|
||||||
|
|
||||||
|
// 검색
|
||||||
|
if (!empty($filters['search'])) {
|
||||||
|
$search = $filters['search'];
|
||||||
|
$query->where(function ($q) use ($search) {
|
||||||
|
$q->where('company_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('business_number', 'like', "%{$search}%")
|
||||||
|
->orWhere('ceo_name', 'like', "%{$search}%")
|
||||||
|
->orWhere('contact_phone', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상태 필터
|
||||||
|
if (!empty($filters['status'])) {
|
||||||
|
$query->where('status', $filters['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 영업파트너 필터
|
||||||
|
if (!empty($filters['registered_by'])) {
|
||||||
|
$query->where('registered_by', $filters['registered_by']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$prospects = $query->orderByDesc('created_at')->paginate(20);
|
||||||
|
|
||||||
|
// 각 가망고객의 진행률 계산
|
||||||
|
foreach ($prospects as $prospect) {
|
||||||
|
$progress = SalesScenarioChecklist::getProspectProgress($prospect->id);
|
||||||
|
$prospect->sales_progress = $progress['sales']['percentage'];
|
||||||
|
$prospect->manager_progress = $progress['manager']['percentage'];
|
||||||
|
|
||||||
|
// management 정보
|
||||||
|
$management = SalesTenantManagement::where('tenant_prospect_id', $prospect->id)->first();
|
||||||
|
$prospect->hq_status = $management?->hq_status ?? 'pending';
|
||||||
|
$prospect->hq_status_label = $management?->hq_status_label ?? '대기';
|
||||||
|
$prospect->manager_user = $management?->manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 전체 통계
|
||||||
|
$stats = [
|
||||||
|
'total' => TenantProspect::count(),
|
||||||
|
'active' => TenantProspect::where('status', TenantProspect::STATUS_ACTIVE)->count(),
|
||||||
|
'expired' => TenantProspect::where('status', TenantProspect::STATUS_EXPIRED)->count(),
|
||||||
|
'converted' => TenantProspect::where('status', TenantProspect::STATUS_CONVERTED)->count(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 영업파트너별 통계
|
||||||
|
$partnerStats = TenantProspect::selectRaw('registered_by, COUNT(*) as total')
|
||||||
|
->groupBy('registered_by')
|
||||||
|
->with('registeredBy')
|
||||||
|
->get()
|
||||||
|
->map(function ($item) {
|
||||||
|
return [
|
||||||
|
'user' => $item->registeredBy,
|
||||||
|
'total' => $item->total,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('sales.admin-prospects.index', compact('prospects', 'stats', 'salesPartners', 'partnerStats', 'filters'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 고객 상세 모달
|
||||||
|
*/
|
||||||
|
public function modalShow(int $id): View
|
||||||
|
{
|
||||||
|
$prospect = TenantProspect::with(['registeredBy', 'tenant'])->findOrFail($id);
|
||||||
|
|
||||||
|
// 진행률
|
||||||
|
$progress = SalesScenarioChecklist::getProspectProgress($prospect->id);
|
||||||
|
$prospect->sales_progress = $progress['sales']['percentage'];
|
||||||
|
$prospect->manager_progress = $progress['manager']['percentage'];
|
||||||
|
|
||||||
|
// management 정보
|
||||||
|
$management = SalesTenantManagement::findOrCreateByProspect($prospect->id);
|
||||||
|
|
||||||
|
return view('sales.admin-prospects.partials.show-modal', compact('prospect', 'management', 'progress'));
|
||||||
|
}
|
||||||
|
}
|
||||||
73
database/seeders/AdminProspectMenuSeeder.php
Normal file
73
database/seeders/AdminProspectMenuSeeder.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Commons\Menu;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 영업파트너 고객관리 메뉴 추가 시더
|
||||||
|
* 관리자/슈퍼관리자 전용 메뉴
|
||||||
|
*/
|
||||||
|
class AdminProspectMenuSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$tenantId = 1;
|
||||||
|
|
||||||
|
// 영업관리 메뉴 ID 찾기
|
||||||
|
$salesParentId = Menu::where('tenant_id', $tenantId)
|
||||||
|
->where('name', '영업관리')
|
||||||
|
->whereNull('parent_id')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if (!$salesParentId) {
|
||||||
|
$this->command->error('영업관리 메뉴를 찾을 수 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 고객 관리 메뉴 찾기 (sort_order 확인)
|
||||||
|
$prospectMenu = Menu::where('tenant_id', $tenantId)
|
||||||
|
->where('parent_id', $salesParentId)
|
||||||
|
->where('name', '고객 관리')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$sortOrder = ($prospectMenu?->sort_order ?? 2) + 1;
|
||||||
|
|
||||||
|
// 영업파트너 고객관리 메뉴 존재 여부 확인
|
||||||
|
$existingMenu = Menu::where('tenant_id', $tenantId)
|
||||||
|
->where('parent_id', $salesParentId)
|
||||||
|
->where('name', '영업파트너 고객관리')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existingMenu) {
|
||||||
|
$this->command->info('영업파트너 고객관리 메뉴가 이미 존재합니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 메뉴 생성
|
||||||
|
Menu::create([
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'parent_id' => $salesParentId,
|
||||||
|
'name' => '영업파트너 고객관리',
|
||||||
|
'url' => '/sales/admin-prospects',
|
||||||
|
'icon' => 'users',
|
||||||
|
'sort_order' => $sortOrder,
|
||||||
|
'is_active' => true,
|
||||||
|
'required_roles' => json_encode(['admin', 'super_admin']), // 관리자만 접근 가능
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->command->info('영업파트너 고객관리 메뉴 생성 완료 (sort_order: ' . $sortOrder . ')');
|
||||||
|
|
||||||
|
// 결과 출력
|
||||||
|
$this->command->info('');
|
||||||
|
$this->command->info('=== 영업관리 하위 메뉴 ===');
|
||||||
|
$children = Menu::where('parent_id', $salesParentId)
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->get(['name', 'url', 'sort_order']);
|
||||||
|
|
||||||
|
foreach ($children as $child) {
|
||||||
|
$this->command->info("{$child->sort_order}. {$child->name} ({$child->url})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
259
resources/views/sales/admin-prospects/index.blade.php
Normal file
259
resources/views/sales/admin-prospects/index.blade.php
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', '영업파트너 고객관리')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<!-- 페이지 헤더 -->
|
||||||
|
<div class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4 mb-6 flex-shrink-0">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800">영업파트너 고객관리</h1>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">전체 영업파트너의 고객 현황을 관리합니다 (관리자 전용)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 통계 카드 -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4 flex-shrink-0">
|
||||||
|
<div class="bg-white rounded-lg shadow-sm p-4">
|
||||||
|
<div class="text-sm text-gray-500">전체 고객</div>
|
||||||
|
<div class="text-2xl font-bold text-gray-800">{{ number_format($stats['total']) }}건</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-blue-50 rounded-lg shadow-sm p-4">
|
||||||
|
<div class="text-sm text-blue-600">영업 진행중</div>
|
||||||
|
<div class="text-2xl font-bold text-blue-800">{{ number_format($stats['active']) }}건</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-orange-50 rounded-lg shadow-sm p-4">
|
||||||
|
<div class="text-sm text-orange-600">영업권 만료</div>
|
||||||
|
<div class="text-2xl font-bold text-orange-800">{{ number_format($stats['expired']) }}건</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-emerald-50 rounded-lg shadow-sm p-4">
|
||||||
|
<div class="text-sm text-emerald-600">계약 완료</div>
|
||||||
|
<div class="text-2xl font-bold text-emerald-800">{{ number_format($stats['converted']) }}건</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 필터 영역 -->
|
||||||
|
<div class="flex-shrink-0 mb-4">
|
||||||
|
<form method="GET" class="flex flex-wrap gap-2 sm:gap-3 items-center bg-white p-3 rounded-lg shadow-sm">
|
||||||
|
<!-- 상태 필터 버튼 -->
|
||||||
|
<div class="flex gap-1 flex-shrink-0">
|
||||||
|
<a href="{{ route('sales.admin-prospects.index', array_merge(request()->except('status', 'page'), [])) }}"
|
||||||
|
class="px-3 py-2 rounded-lg text-sm font-medium transition {{ !request('status') ? 'bg-gray-800 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
|
||||||
|
전체
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('sales.admin-prospects.index', array_merge(request()->except('page'), ['status' => 'active'])) }}"
|
||||||
|
class="px-3 py-2 rounded-lg text-sm font-medium transition {{ request('status') === 'active' ? 'bg-blue-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
|
||||||
|
진행중
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('sales.admin-prospects.index', array_merge(request()->except('page'), ['status' => 'converted'])) }}"
|
||||||
|
class="px-3 py-2 rounded-lg text-sm font-medium transition {{ request('status') === 'converted' ? 'bg-emerald-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
|
||||||
|
계약완료
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 구분선 -->
|
||||||
|
<div class="hidden sm:block w-px h-8 bg-gray-300"></div>
|
||||||
|
|
||||||
|
<!-- 현재 status 유지 -->
|
||||||
|
@if(request('status'))
|
||||||
|
<input type="hidden" name="status" value="{{ request('status') }}">
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- 영업파트너 선택 -->
|
||||||
|
<div class="w-40 flex-shrink-0">
|
||||||
|
<select name="registered_by" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
|
||||||
|
<option value="">전체 파트너</option>
|
||||||
|
@foreach($salesPartners as $partner)
|
||||||
|
<option value="{{ $partner->id }}" {{ request('registered_by') == $partner->id ? 'selected' : '' }}>
|
||||||
|
{{ $partner->name }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 검색 입력 -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<input type="text"
|
||||||
|
name="search"
|
||||||
|
value="{{ request('search') }}"
|
||||||
|
placeholder="업체명, 사업자번호, 대표자, 연락처 검색..."
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 검색 버튼 -->
|
||||||
|
<button type="submit" class="bg-gray-700 hover:bg-gray-800 text-white px-4 py-2 rounded-lg transition text-sm flex-shrink-0">
|
||||||
|
검색
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 테이블 -->
|
||||||
|
<div class="bg-white rounded-lg shadow-sm overflow-hidden flex-1 flex flex-col min-h-0">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">업체명</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">담당 파트너</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">담당 매니저</th>
|
||||||
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">영업 진행률</th>
|
||||||
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">매니저 진행률</th>
|
||||||
|
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">개발 상태</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">상태</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">등록일</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">관리</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
@forelse($prospects as $prospect)
|
||||||
|
<tr class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
|
<div class="font-medium text-gray-900">{{ $prospect->company_name }}</div>
|
||||||
|
<div class="text-xs text-gray-500">{{ $prospect->business_number }}</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
|
@if($prospect->registeredBy)
|
||||||
|
<span class="text-sm text-gray-900">{{ $prospect->registeredBy->name }}</span>
|
||||||
|
@else
|
||||||
|
<span class="text-sm text-gray-400">-</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
|
@if($prospect->manager_user)
|
||||||
|
<span class="text-sm text-gray-900">{{ $prospect->manager_user->name }}</span>
|
||||||
|
@else
|
||||||
|
<span class="text-sm text-gray-400">미지정</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-center">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<div class="w-16 bg-gray-200 rounded-full h-2">
|
||||||
|
<div class="bg-blue-500 h-2 rounded-full" style="width: {{ $prospect->sales_progress }}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-600">{{ $prospect->sales_progress }}%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-center">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<div class="w-16 bg-gray-200 rounded-full h-2">
|
||||||
|
<div class="bg-green-500 h-2 rounded-full" style="width: {{ $prospect->manager_progress }}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs text-gray-600">{{ $prospect->manager_progress }}%</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-center">
|
||||||
|
<span class="px-2 py-1 text-xs font-medium rounded-full
|
||||||
|
@if($prospect->hq_status === 'handover') bg-emerald-100 text-emerald-700
|
||||||
|
@elseif($prospect->hq_status === 'pending') bg-gray-100 text-gray-600
|
||||||
|
@else bg-purple-100 text-purple-700 @endif">
|
||||||
|
{{ $prospect->hq_status_label }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap">
|
||||||
|
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $prospect->status_color }}">
|
||||||
|
{{ $prospect->status_label }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
|
||||||
|
{{ $prospect->created_at->format('Y-m-d') }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<button type="button" onclick="openDetailModal({{ $prospect->id }})" class="text-blue-600 hover:text-blue-900">상세</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr>
|
||||||
|
<td colspan="9" class="px-6 py-12 text-center text-gray-500">
|
||||||
|
등록된 고객이 없습니다.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 페이지네이션 -->
|
||||||
|
@if($prospects->hasPages())
|
||||||
|
<div class="px-6 py-4 border-t border-gray-200">
|
||||||
|
{{ $prospects->withQueryString()->links() }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 상세 모달 -->
|
||||||
|
<div id="detailModal" class="hidden fixed inset-0 z-50 overflow-y-auto">
|
||||||
|
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"></div>
|
||||||
|
<div class="flex min-h-full items-center justify-center p-4">
|
||||||
|
<div id="detailModalContent" class="relative bg-white rounded-xl shadow-2xl w-full max-w-3xl">
|
||||||
|
<div class="p-12 text-center">
|
||||||
|
<svg class="w-8 h-8 animate-spin text-blue-600 mx-auto" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="mt-2 text-gray-500">로딩 중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script>
|
||||||
|
function openDetailModal(id) {
|
||||||
|
const modal = document.getElementById('detailModal');
|
||||||
|
const content = document.getElementById('detailModalContent');
|
||||||
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="p-12 text-center">
|
||||||
|
<svg class="w-8 h-8 animate-spin text-blue-600 mx-auto" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="mt-2 text-gray-500">로딩 중...</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
fetch(`/sales/admin-prospects/${id}/modal-show`, {
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Accept': 'text/html'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
content.innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="p-6 text-center">
|
||||||
|
<p class="text-red-500">오류가 발생했습니다.</p>
|
||||||
|
<button onclick="closeDetailModal()" class="mt-4 px-4 py-2 bg-gray-600 text-white rounded-lg">닫기</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDetailModal() {
|
||||||
|
const modal = document.getElementById('detailModal');
|
||||||
|
modal.classList.add('hidden');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeDetailModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (e.target.closest('[data-close-modal]')) {
|
||||||
|
e.preventDefault();
|
||||||
|
closeDetailModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
{{-- 상세 모달 내용 --}}
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- 헤더 -->
|
||||||
|
<div class="flex items-center justify-between mb-6 pb-4 border-b border-gray-200">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-bold text-gray-900">{{ $prospect->company_name }}</h2>
|
||||||
|
<p class="text-sm text-gray-500">{{ $prospect->business_number }}</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" data-close-modal class="text-gray-400 hover:text-gray-600">
|
||||||
|
<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 class="grid grid-cols-2 gap-4 mb-6">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500">대표자</p>
|
||||||
|
<p class="font-medium">{{ $prospect->ceo_name ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500">연락처</p>
|
||||||
|
<p class="font-medium">{{ $prospect->contact_phone ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500">이메일</p>
|
||||||
|
<p class="font-medium">{{ $prospect->contact_email ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500">상태</p>
|
||||||
|
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $prospect->status_color }}">
|
||||||
|
{{ $prospect->status_label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 담당자 정보 -->
|
||||||
|
<div class="bg-gray-50 rounded-lg p-4 mb-6">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">담당자 정보</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500">담당 파트너</p>
|
||||||
|
<p class="font-medium">{{ $prospect->registeredBy?->name ?? '-' }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm text-gray-500">담당 매니저</p>
|
||||||
|
<p class="font-medium">{{ $management->manager?->name ?? '미지정' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 진행 현황 -->
|
||||||
|
<div class="bg-blue-50 rounded-lg p-4 mb-6">
|
||||||
|
<h3 class="text-sm font-semibold text-blue-700 mb-3">영업 진행 현황</h3>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-sm mb-1">
|
||||||
|
<span class="text-blue-600">영업 시나리오</span>
|
||||||
|
<span class="font-medium">{{ $progress['sales']['percentage'] }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-blue-200 rounded-full h-2">
|
||||||
|
<div class="bg-blue-500 h-2 rounded-full" style="width: {{ $progress['sales']['percentage'] }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between text-sm mb-1">
|
||||||
|
<span class="text-green-600">매니저 시나리오</span>
|
||||||
|
<span class="font-medium">{{ $progress['manager']['percentage'] }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-green-200 rounded-full h-2">
|
||||||
|
<div class="bg-green-500 h-2 rounded-full" style="width: {{ $progress['manager']['percentage'] }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 개발 진행 상태 -->
|
||||||
|
<div class="bg-purple-50 rounded-lg p-4 mb-6">
|
||||||
|
<h3 class="text-sm font-semibold text-purple-700 mb-3">개발 진행 상태</h3>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="px-3 py-1 text-sm font-medium rounded-full
|
||||||
|
@if($management->hq_status === 'handover') bg-emerald-100 text-emerald-700
|
||||||
|
@elseif($management->hq_status === 'pending') bg-gray-100 text-gray-600
|
||||||
|
@else bg-purple-100 text-purple-700 @endif">
|
||||||
|
{{ $management->hq_status_label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 등록 정보 -->
|
||||||
|
<div class="text-sm text-gray-500">
|
||||||
|
<p>등록일: {{ $prospect->created_at->format('Y-m-d H:i') }}</p>
|
||||||
|
@if($prospect->expires_at)
|
||||||
|
<p>영업권 만료: {{ $prospect->expires_at->format('Y-m-d') }}</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 버튼 -->
|
||||||
|
<div class="flex justify-end gap-2 mt-6 pt-4 border-t border-gray-200">
|
||||||
|
<button type="button" data-close-modal class="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition">
|
||||||
|
닫기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -928,6 +928,10 @@
|
|||||||
Route::get('prospects/{id}/modal-show', [\App\Http\Controllers\Sales\TenantProspectController::class, 'modalShow'])->name('prospects.modal-show');
|
Route::get('prospects/{id}/modal-show', [\App\Http\Controllers\Sales\TenantProspectController::class, 'modalShow'])->name('prospects.modal-show');
|
||||||
Route::get('prospects/{id}/modal-edit', [\App\Http\Controllers\Sales\TenantProspectController::class, 'modalEdit'])->name('prospects.modal-edit');
|
Route::get('prospects/{id}/modal-edit', [\App\Http\Controllers\Sales\TenantProspectController::class, 'modalEdit'])->name('prospects.modal-edit');
|
||||||
|
|
||||||
|
// 관리자용 전체 고객 관리 (관리자/슈퍼관리자 전용)
|
||||||
|
Route::get('admin-prospects', [\App\Http\Controllers\Sales\AdminProspectController::class, 'index'])->name('admin-prospects.index');
|
||||||
|
Route::get('admin-prospects/{id}/modal-show', [\App\Http\Controllers\Sales\AdminProspectController::class, 'modalShow'])->name('admin-prospects.modal-show');
|
||||||
|
|
||||||
// 영업 시나리오 관리
|
// 영업 시나리오 관리
|
||||||
Route::prefix('scenarios')->name('scenarios.')->group(function () {
|
Route::prefix('scenarios')->name('scenarios.')->group(function () {
|
||||||
// 테넌트 기반 (기존)
|
// 테넌트 기반 (기존)
|
||||||
|
|||||||
Reference in New Issue
Block a user