feat:영업관리 대시보드 HTMX 부분 새로고침 구현

- 기간별 조회 및 실적 새로고침 시 전체 페이지가 아닌 데이터 영역만 갱신
- partial 뷰 분리 (stats, commission-by-role, tenant-stats, no-data)
- 컨트롤러에 refresh 메서드 추가
- 로딩 인디케이터 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
pro
2026-01-27 19:28:48 +09:00
parent 6b7b339d66
commit 0e88660c89
8 changed files with 375 additions and 345 deletions

View File

@@ -15,6 +15,26 @@ class SalesDashboardController extends Controller
* 대시보드 화면
*/
public function index(Request $request): View
{
$data = $this->getDashboardData($request);
return view('sales.dashboard.index', $data);
}
/**
* HTMX 부분 새로고침용 데이터 반환
*/
public function refresh(Request $request): View
{
$data = $this->getDashboardData($request);
return view('sales.dashboard.partials.data-container', $data);
}
/**
* 대시보드 데이터 조회
*/
private function getDashboardData(Request $request): array
{
// 기간 설정
$period = $request->input('period', 'month'); // month or custom
@@ -73,7 +93,7 @@ public function index(Request $request): View
'confirmed_commission' => 0, // 확정 가입비 수당
];
return view('sales.dashboard.index', compact(
return compact(
'stats',
'commissionByRole',
'totalCommissionRatio',
@@ -83,6 +103,6 @@ public function index(Request $request): View
'month',
'startDate',
'endDate'
));
);
}
}

View File

@@ -2,6 +2,20 @@
@section('title', '영업관리 대시보드')
@push('styles')
<style>
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline-flex;
}
.htmx-request.htmx-indicator {
display: inline-flex;
}
</style>
@endpush
@section('content')
<div class="space-y-6">
<!-- 페이지 헤더 -->
@@ -24,349 +38,9 @@
</div>
</div>
<!-- 전체 누적 실적 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<h2 class="text-lg font-bold text-gray-800 mb-4">전체 누적 실적</h2>
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<!-- 가입비 -->
<div class="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<span class="text-sm text-gray-500"> 가입비</span>
<div class="p-2 bg-gray-100 rounded-lg">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
</div>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ number_format($stats['total_membership_fee']) }}</p>
<p class="text-xs text-gray-400 mt-1">전체 누적 가입비</p>
</div>
<!-- 수당 -->
<div class="bg-blue-50 border border-blue-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<span class="text-sm text-blue-600"> 수당</span>
<div class="p-2 bg-blue-100 rounded-lg">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
</div>
<p class="text-2xl font-bold text-blue-700 mt-2">{{ number_format($stats['total_commission']) }}</p>
<p class="text-xs text-blue-500 mt-1">지급 승인 완료 기준 ({{ $stats['commission_rate'] }}%)</p>
</div>
<!-- 전체 건수 -->
<div class="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<span class="text-sm text-gray-500">전체 건수</span>
<div class="p-2 bg-gray-100 rounded-lg">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</div>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ number_format($stats['total_contracts']) }}</p>
<p class="text-xs text-gray-400 mt-1">전체 계약 건수</p>
</div>
<!-- 가입 승인 대기 -->
<div class="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<span class="text-sm text-gray-500">가입 승인 대기</span>
<div class="p-2 bg-gray-100 rounded-lg">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
</div>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ number_format($stats['pending_membership_approval']) }}</p>
<p class="text-xs text-gray-400 mt-1">조직 가입 승인 대기</p>
</div>
<!-- 지급 승인 대기 -->
<div class="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<span class="text-sm text-gray-500">지급 승인 대기</span>
<div class="p-2 bg-gray-100 rounded-lg">
<svg class="w-5 h-5 text-gray-600" 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-6 9l2 2 4-4" />
</svg>
</div>
</div>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ number_format($stats['pending_payment_approval']) }}</p>
<p class="text-xs text-gray-400 mt-1">조직 지급 승인 대기</p>
</div>
</div>
</div>
<!-- 기간별 조회 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center gap-3 mb-4">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<h2 class="text-lg font-bold text-gray-800">기간별 조회</h2>
</div>
<div class="flex flex-wrap items-center gap-3">
<div class="inline-flex rounded-lg border border-gray-200 p-1">
<button type="button"
id="btn-month"
class="px-4 py-2 text-sm font-medium rounded-md transition-colors {{ $period === 'month' ? 'bg-blue-600 text-white' : 'text-gray-600 hover:bg-gray-100' }}"
onclick="togglePeriodMode('month')">
당월
</button>
<button type="button"
id="btn-custom"
class="px-4 py-2 text-sm font-medium rounded-md transition-colors {{ $period === 'custom' ? 'bg-blue-600 text-white' : 'text-gray-600 hover:bg-gray-100' }}"
onclick="togglePeriodMode('custom')">
기간 설정
</button>
</div>
<!-- 당월 표시 -->
<div id="month-display" class="{{ $period === 'custom' ? 'hidden' : '' }}">
<span class="text-sm text-gray-600">{{ $year }} {{ $month }}</span>
</div>
<!-- 기간 설정 입력 -->
<div id="custom-period" class="flex items-center gap-2 {{ $period === 'month' ? 'hidden' : '' }}">
<input type="date"
id="start-date"
value="{{ $startDate ?? now()->startOfMonth()->format('Y-m-d') }}"
class="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<span class="text-gray-500">~</span>
<input type="date"
id="end-date"
value="{{ $endDate ?? now()->format('Y-m-d') }}"
class="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<button type="button"
onclick="applyCustomPeriod()"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors">
조회
</button>
</div>
</div>
</div>
<!-- 역할별 수당 상세 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center gap-3 mb-4">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<h2 class="text-lg font-bold text-gray-800">역할별 수당 상세</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
@foreach($commissionByRole as $role)
<div class="rounded-xl p-4 border
@if($role['color'] === 'green') bg-green-50 border-green-200
@elseif($role['color'] === 'blue') bg-blue-50 border-blue-200
@elseif($role['color'] === 'red') bg-red-50 border-red-200
@else bg-gray-50 border-gray-200
@endif">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<svg class="w-5 h-5
@if($role['color'] === 'green') text-green-600
@elseif($role['color'] === 'blue') text-blue-600
@elseif($role['color'] === 'red') text-red-600
@else text-gray-600
@endif" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-sm font-medium
@if($role['color'] === 'green') text-green-800
@elseif($role['color'] === 'blue') text-blue-800
@elseif($role['color'] === 'red') text-red-800
@else text-gray-800
@endif">{{ $role['name'] }}</span>
</div>
@if($role['rate'] !== null)
<span class="text-sm font-semibold
@if($role['color'] === 'green') text-green-600
@elseif($role['color'] === 'blue') text-blue-600
@else text-gray-600
@endif">{{ $role['rate'] }}%</span>
@else
<span class="px-2 py-0.5 text-xs font-medium bg-red-100 text-red-700 rounded">별도</span>
@endif
</div>
@if($role['amount'] !== null)
<p class="text-2xl font-bold
@if($role['color'] === 'green') text-green-700
@elseif($role['color'] === 'blue') text-blue-700
@else text-gray-700
@endif">{{ number_format($role['amount']) }}</p>
@else
<p class="text-xl font-bold text-red-600">운영팀 산정</p>
@endif
</div>
@endforeach
</div>
<!-- 가입비 대비 수당 -->
<div class="bg-gray-50 rounded-xl p-4 border border-gray-200">
<div class="flex items-center justify-end gap-4">
<div class="text-right">
<p class="text-2xl font-bold text-gray-900">{{ number_format($totalCommissionRatio) }}</p>
<p class="text-sm text-gray-500"> 가입비 대비 수당</p>
</div>
</div>
</div>
</div>
<!-- 실적 데이터 없음 안내 (데이터가 없을 때만 표시) -->
@if($stats['total_contracts'] == 0)
<div class="bg-white rounded-xl shadow-sm p-12">
<div class="flex flex-col items-center justify-center text-center">
<div class="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center mb-6">
<svg class="w-10 h-10 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-2">실적 데이터가 존재하지 않습니다</h3>
<p class="text-gray-500 mb-2">선택한 기간 내에 등록된 계약 정보나 조직 구성 데이터가 없습니다.</p>
<p class="text-gray-500 mb-6">아직 실적이 발생하지 않았거나, 시스템 동기화 중일 있습니다.</p>
<button type="button"
onclick="location.reload()"
class="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
실적 데이터 새로고침
</button>
</div>
</div>
@endif
<!-- 수익 테넌트 관리 -->
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center gap-3 mb-6">
<div class="p-2 bg-blue-100 rounded-lg">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h2 class="text-xl font-bold text-gray-800">수익 테넌트 관리</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- 관리 테넌트 -->
<div class="bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between mb-3">
<span class="text-sm text-gray-500">관리 테넌트</span>
<div class="p-2 bg-blue-50 rounded-lg">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
</div>
<p class="text-3xl font-bold text-gray-900">{{ number_format($tenantStats['total_tenants'] ?? 0) }}</p>
<p class="text-xs text-gray-400 mt-1">등록된 업체 </p>
</div>
<!-- 가입비 실적 -->
<div class="bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between mb-3">
<span class="text-sm text-gray-500"> 가입비 실적</span>
<div class="p-2 bg-blue-50 rounded-lg">
<svg class="w-5 h-5 text-blue-600" 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>
</div>
</div>
<p class="text-3xl font-bold text-gray-900">{{ number_format($tenantStats['total_membership_revenue'] ?? 0) }}</p>
<p class="text-xs text-gray-400 mt-1">전체 가입비 합계</p>
</div>
<!-- 누적 가입비 수당 -->
<div class="bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between mb-3">
<span class="text-sm text-gray-500">누적 가입비 수당</span>
<div class="p-2 bg-blue-50 rounded-lg">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<p class="text-3xl font-bold text-gray-900">{{ number_format($tenantStats['total_commission_accumulated'] ?? 0) }}</p>
<p class="text-xs text-gray-400 mt-1">전체 가입비 수당 합계</p>
</div>
<!-- 확정 가입비 수당 (지급대상) -->
<div class="bg-green-50 border border-green-200 rounded-xl p-5 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between mb-3">
<span class="text-sm text-green-700">확정 가입비 수당 (지급대상)</span>
<div class="p-2 bg-green-100 rounded-lg">
<svg class="w-5 h-5 text-green-600" 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>
</div>
</div>
<p class="text-3xl font-bold text-green-700">{{ number_format($tenantStats['confirmed_commission'] ?? 0) }}</p>
<p class="text-xs text-green-600 mt-1">운영팀 승인 완료된 금액 (지급: 계약 익월 말일)</p>
</div>
</div>
<!-- 대시보드 데이터 컨테이너 (HTMX로 새로고침되는 영역) -->
<div id="dashboard-data" class="space-y-6">
@include('sales.dashboard.partials.data-container')
</div>
</div>
@push('scripts')
<script>
function togglePeriodMode(mode) {
const btnMonth = document.getElementById('btn-month');
const btnCustom = document.getElementById('btn-custom');
const monthDisplay = document.getElementById('month-display');
const customPeriod = document.getElementById('custom-period');
if (mode === 'month') {
// 당월 모드
btnMonth.classList.add('bg-blue-600', 'text-white');
btnMonth.classList.remove('text-gray-600', 'hover:bg-gray-100');
btnCustom.classList.remove('bg-blue-600', 'text-white');
btnCustom.classList.add('text-gray-600', 'hover:bg-gray-100');
monthDisplay.classList.remove('hidden');
customPeriod.classList.add('hidden');
// 당월로 이동
const url = new URL(window.location);
url.searchParams.set('period', 'month');
url.searchParams.delete('start_date');
url.searchParams.delete('end_date');
window.location = url;
} else {
// 기간 설정 모드
btnCustom.classList.add('bg-blue-600', 'text-white');
btnCustom.classList.remove('text-gray-600', 'hover:bg-gray-100');
btnMonth.classList.remove('bg-blue-600', 'text-white');
btnMonth.classList.add('text-gray-600', 'hover:bg-gray-100');
customPeriod.classList.remove('hidden');
monthDisplay.classList.add('hidden');
}
}
function applyCustomPeriod() {
const startDate = document.getElementById('start-date').value;
const endDate = document.getElementById('end-date').value;
if (!startDate || !endDate) {
alert('시작일과 종료일을 모두 선택해주세요.');
return;
}
if (startDate > endDate) {
alert('시작일은 종료일보다 이전이어야 합니다.');
return;
}
const url = new URL(window.location);
url.searchParams.set('period', 'custom');
url.searchParams.set('start_date', startDate);
url.searchParams.set('end_date', endDate);
window.location = url;
}
</script>
@endpush
@endsection

View File

@@ -0,0 +1,67 @@
{{-- 역할별 수당 상세 --}}
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center gap-3 mb-4">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<h2 class="text-lg font-bold text-gray-800">역할별 수당 상세</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
@foreach($commissionByRole as $role)
<div class="rounded-xl p-4 border
@if($role['color'] === 'green') bg-green-50 border-green-200
@elseif($role['color'] === 'blue') bg-blue-50 border-blue-200
@elseif($role['color'] === 'red') bg-red-50 border-red-200
@else bg-gray-50 border-gray-200
@endif">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<svg class="w-5 h-5
@if($role['color'] === 'green') text-green-600
@elseif($role['color'] === 'blue') text-blue-600
@elseif($role['color'] === 'red') text-red-600
@else text-gray-600
@endif" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span class="text-sm font-medium
@if($role['color'] === 'green') text-green-800
@elseif($role['color'] === 'blue') text-blue-800
@elseif($role['color'] === 'red') text-red-800
@else text-gray-800
@endif">{{ $role['name'] }}</span>
</div>
@if($role['rate'] !== null)
<span class="text-sm font-semibold
@if($role['color'] === 'green') text-green-600
@elseif($role['color'] === 'blue') text-blue-600
@else text-gray-600
@endif">{{ $role['rate'] }}%</span>
@else
<span class="px-2 py-0.5 text-xs font-medium bg-red-100 text-red-700 rounded">별도</span>
@endif
</div>
@if($role['amount'] !== null)
<p class="text-2xl font-bold
@if($role['color'] === 'green') text-green-700
@elseif($role['color'] === 'blue') text-blue-700
@else text-gray-700
@endif">{{ number_format($role['amount']) }}</p>
@else
<p class="text-xl font-bold text-red-600">운영팀 산정</p>
@endif
</div>
@endforeach
</div>
<!-- 가입비 대비 수당 -->
<div class="bg-gray-50 rounded-xl p-4 border border-gray-200">
<div class="flex items-center justify-end gap-4">
<div class="text-right">
<p class="text-2xl font-bold text-gray-900">{{ number_format($totalCommissionRatio) }}</p>
<p class="text-sm text-gray-500"> 가입비 대비 수당</p>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,98 @@
{{-- 대시보드 데이터 컨테이너 (HTMX로 새로고침되는 영역) --}}
{{-- 전체 누적 실적 --}}
@include('sales.dashboard.partials.stats')
{{-- 기간별 조회 --}}
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center gap-3 mb-4">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<h2 class="text-lg font-bold text-gray-800">기간별 조회</h2>
</div>
<form id="period-form" class="flex flex-wrap items-center gap-3">
<div class="inline-flex rounded-lg border border-gray-200 p-1">
<button type="button"
id="btn-month"
hx-get="{{ route('sales.salesmanagement.dashboard.refresh') }}"
hx-target="#dashboard-data"
hx-swap="innerHTML"
hx-vals='{"period": "month"}'
hx-indicator="#loading-indicator"
class="px-4 py-2 text-sm font-medium rounded-md transition-colors {{ $period === 'month' ? 'bg-blue-600 text-white' : 'text-gray-600 hover:bg-gray-100' }}">
당월
</button>
<button type="button"
id="btn-custom"
class="px-4 py-2 text-sm font-medium rounded-md transition-colors {{ $period === 'custom' ? 'bg-blue-600 text-white' : 'text-gray-600 hover:bg-gray-100' }}"
onclick="toggleCustomPeriod()">
기간 설정
</button>
</div>
<!-- 당월 표시 -->
<div id="month-display" class="{{ $period === 'custom' ? 'hidden' : '' }}">
<span class="text-sm text-gray-600">{{ $year }} {{ $month }}</span>
</div>
<!-- 기간 설정 입력 -->
<div id="custom-period" class="flex items-center gap-2 {{ $period === 'month' ? 'hidden' : '' }}">
<input type="date"
name="start_date"
id="start-date"
value="{{ $startDate ?? now()->startOfMonth()->format('Y-m-d') }}"
class="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<span class="text-gray-500">~</span>
<input type="date"
name="end_date"
id="end-date"
value="{{ $endDate ?? now()->format('Y-m-d') }}"
class="px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
<button type="button"
hx-get="{{ route('sales.salesmanagement.dashboard.refresh') }}"
hx-target="#dashboard-data"
hx-swap="innerHTML"
hx-include="#start-date, #end-date"
hx-vals='{"period": "custom"}'
hx-indicator="#loading-indicator"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 transition-colors">
조회
</button>
</div>
<!-- 로딩 인디케이터 -->
<div id="loading-indicator" class="htmx-indicator">
<svg class="w-5 h-5 animate-spin text-blue-600" 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>
</div>
</form>
</div>
{{-- 역할별 수당 상세 --}}
@include('sales.dashboard.partials.commission-by-role')
{{-- 실적 데이터 없음 안내 --}}
@include('sales.dashboard.partials.no-data')
{{-- 수익 테넌트 관리 --}}
@include('sales.dashboard.partials.tenant-stats')
<script>
function toggleCustomPeriod() {
const btnMonth = document.getElementById('btn-month');
const btnCustom = document.getElementById('btn-custom');
const monthDisplay = document.getElementById('month-display');
const customPeriod = document.getElementById('custom-period');
btnCustom.classList.add('bg-blue-600', 'text-white');
btnCustom.classList.remove('text-gray-600', 'hover:bg-gray-100');
btnMonth.classList.remove('bg-blue-600', 'text-white');
btnMonth.classList.add('text-gray-600', 'hover:bg-gray-100');
customPeriod.classList.remove('hidden');
monthDisplay.classList.add('hidden');
}
</script>

View File

@@ -0,0 +1,26 @@
{{-- 실적 데이터 없음 안내 --}}
@if($stats['total_contracts'] == 0)
<div class="bg-white rounded-xl shadow-sm p-12">
<div class="flex flex-col items-center justify-center text-center">
<div class="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center mb-6">
<svg class="w-10 h-10 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
</svg>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-2">실적 데이터가 존재하지 않습니다</h3>
<p class="text-gray-500 mb-2">선택한 기간 내에 등록된 계약 정보나 조직 구성 데이터가 없습니다.</p>
<p class="text-gray-500 mb-6">아직 실적이 발생하지 않았거나, 시스템 동기화 중일 있습니다.</p>
<button type="button"
hx-get="{{ route('sales.salesmanagement.dashboard.refresh') }}"
hx-target="#dashboard-data"
hx-swap="innerHTML"
hx-indicator="#refresh-indicator"
class="inline-flex items-center gap-2 px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors">
<svg class="w-5 h-5 htmx-indicator" id="refresh-indicator" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
실적 데이터 새로고침
</button>
</div>
</div>
@endif

View File

@@ -0,0 +1,75 @@
{{-- 전체 누적 실적 --}}
<div class="bg-white rounded-xl shadow-sm p-6">
<h2 class="text-lg font-bold text-gray-800 mb-4">전체 누적 실적</h2>
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
<!-- 가입비 -->
<div class="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<span class="text-sm text-gray-500"> 가입비</span>
<div class="p-2 bg-gray-100 rounded-lg">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
</div>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ number_format($stats['total_membership_fee']) }}</p>
<p class="text-xs text-gray-400 mt-1">전체 누적 가입비</p>
</div>
<!-- 수당 -->
<div class="bg-blue-50 border border-blue-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<span class="text-sm text-blue-600"> 수당</span>
<div class="p-2 bg-blue-100 rounded-lg">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
</div>
<p class="text-2xl font-bold text-blue-700 mt-2">{{ number_format($stats['total_commission']) }}</p>
<p class="text-xs text-blue-500 mt-1">지급 승인 완료 기준 ({{ $stats['commission_rate'] }}%)</p>
</div>
<!-- 전체 건수 -->
<div class="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<span class="text-sm text-gray-500">전체 건수</span>
<div class="p-2 bg-gray-100 rounded-lg">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
</div>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ number_format($stats['total_contracts']) }}</p>
<p class="text-xs text-gray-400 mt-1">전체 계약 건수</p>
</div>
<!-- 가입 승인 대기 -->
<div class="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<span class="text-sm text-gray-500">가입 승인 대기</span>
<div class="p-2 bg-gray-100 rounded-lg">
<svg class="w-5 h-5 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
</div>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ number_format($stats['pending_membership_approval']) }}</p>
<p class="text-xs text-gray-400 mt-1">조직 가입 승인 대기</p>
</div>
<!-- 지급 승인 대기 -->
<div class="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between">
<span class="text-sm text-gray-500">지급 승인 대기</span>
<div class="p-2 bg-gray-100 rounded-lg">
<svg class="w-5 h-5 text-gray-600" 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-6 9l2 2 4-4" />
</svg>
</div>
</div>
<p class="text-2xl font-bold text-gray-900 mt-2">{{ number_format($stats['pending_payment_approval']) }}</p>
<p class="text-xs text-gray-400 mt-1">조직 지급 승인 대기</p>
</div>
</div>
</div>

View File

@@ -0,0 +1,69 @@
{{-- 수익 테넌트 관리 --}}
<div class="bg-white rounded-xl shadow-sm p-6">
<div class="flex items-center gap-3 mb-6">
<div class="p-2 bg-blue-100 rounded-lg">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<h2 class="text-xl font-bold text-gray-800">수익 테넌트 관리</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- 관리 테넌트 -->
<div class="bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between mb-3">
<span class="text-sm text-gray-500">관리 테넌트</span>
<div class="p-2 bg-blue-50 rounded-lg">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
</div>
<p class="text-3xl font-bold text-gray-900">{{ number_format($tenantStats['total_tenants'] ?? 0) }}</p>
<p class="text-xs text-gray-400 mt-1">등록된 업체 </p>
</div>
<!-- 가입비 실적 -->
<div class="bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between mb-3">
<span class="text-sm text-gray-500"> 가입비 실적</span>
<div class="p-2 bg-blue-50 rounded-lg">
<svg class="w-5 h-5 text-blue-600" 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>
</div>
</div>
<p class="text-3xl font-bold text-gray-900">{{ number_format($tenantStats['total_membership_revenue'] ?? 0) }}</p>
<p class="text-xs text-gray-400 mt-1">전체 가입비 합계</p>
</div>
<!-- 누적 가입비 수당 -->
<div class="bg-white border border-gray-200 rounded-xl p-5 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between mb-3">
<span class="text-sm text-gray-500">누적 가입비 수당</span>
<div class="p-2 bg-blue-50 rounded-lg">
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<p class="text-3xl font-bold text-gray-900">{{ number_format($tenantStats['total_commission_accumulated'] ?? 0) }}</p>
<p class="text-xs text-gray-400 mt-1">전체 가입비 수당 합계</p>
</div>
<!-- 확정 가입비 수당 (지급대상) -->
<div class="bg-green-50 border border-green-200 rounded-xl p-5 hover:shadow-md transition-shadow">
<div class="flex items-start justify-between mb-3">
<span class="text-sm text-green-700">확정 가입비 수당 (지급대상)</span>
<div class="p-2 bg-green-100 rounded-lg">
<svg class="w-5 h-5 text-green-600" 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>
</div>
</div>
<p class="text-3xl font-bold text-green-700">{{ number_format($tenantStats['confirmed_commission'] ?? 0) }}</p>
<p class="text-xs text-green-600 mt-1">운영팀 승인 완료된 금액 (지급: 계약 익월 말일)</p>
</div>
</div>
</div>

View File

@@ -790,6 +790,7 @@
Route::middleware(['auth', 'hq.member'])->prefix('sales')->name('sales.')->group(function () {
// 영업관리 대시보드
Route::get('salesmanagement/dashboard', [\App\Http\Controllers\Sales\SalesDashboardController::class, 'index'])->name('salesmanagement.dashboard');
Route::get('salesmanagement/dashboard/refresh', [\App\Http\Controllers\Sales\SalesDashboardController::class, 'refresh'])->name('salesmanagement.dashboard.refresh');
// 영업 담당자 관리
Route::resource('managers', \App\Http\Controllers\Sales\SalesManagerController::class);