feat:수당지급현황통계 페이지 고도화 (종합 대시보드)
- 필터: 년/월 범위, 상태, 지급유형, 파트너, 매니저, 검색어 - 통계 카드 4→8개 (총 발생액, 지급완료, 미지급, 파트너수, 유형별 합계, 평균) - 차트 4→6개 (월별 추이, 유형비율, Top10, 상태분포 건수/금액, 파트너vs매니저) - 테이블 1→3개 탭 (월별 요약, 파트너별 결산, 매니저별 결산 + 완료율 프로그레스바) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,12 @@
|
||||
use App\Models\Sales\SalesContractProduct;
|
||||
use App\Models\Sales\SalesPartner;
|
||||
use App\Models\Sales\SalesTenantManagement;
|
||||
use App\Models\User;
|
||||
use App\Services\SalesCommissionService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SettlementController extends Controller
|
||||
@@ -365,73 +368,259 @@ public function paymentStats(Request $request): View|Response
|
||||
return response('', 200)->header('HX-Redirect', route('finance.settlement.payment-stats'));
|
||||
}
|
||||
|
||||
$year = (int) $request->input('year', now()->year);
|
||||
// 필터 파라미터
|
||||
$now = now();
|
||||
$startYear = (int) $request->input('start_year', $now->year);
|
||||
$startMonth = (int) $request->input('start_month', 1);
|
||||
$endYear = (int) $request->input('end_year', $now->year);
|
||||
$endMonth = (int) $request->input('end_month', $now->month);
|
||||
$status = $request->input('status');
|
||||
$paymentType = $request->input('payment_type');
|
||||
$partnerId = $request->input('partner_id');
|
||||
$managerUserId = $request->input('manager_user_id');
|
||||
$search = $request->input('search');
|
||||
|
||||
// 통계 카드
|
||||
$totalPaidAmount = SalesCommission::where('status', SalesCommission::STATUS_PAID)
|
||||
->whereYear('actual_payment_date', $year)
|
||||
->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total')
|
||||
->value('total') ?? 0;
|
||||
$startDate = Carbon::create($startYear, $startMonth, 1)->startOfMonth();
|
||||
$endDate = Carbon::create($endYear, $endMonth, 1)->endOfMonth();
|
||||
|
||||
$totalPaidCount = SalesCommission::where('status', SalesCommission::STATUS_PAID)
|
||||
->whereYear('actual_payment_date', $year)
|
||||
->count();
|
||||
// 공통 baseQuery 클로저
|
||||
$baseQuery = function () use ($startDate, $endDate, $status, $paymentType, $partnerId, $managerUserId, $search) {
|
||||
$query = SalesCommission::query()
|
||||
->whereBetween('scheduled_payment_date', [$startDate, $endDate]);
|
||||
|
||||
$activePartners = SalesCommission::where('status', SalesCommission::STATUS_PAID)
|
||||
->whereYear('actual_payment_date', $year)
|
||||
->distinct('partner_id')
|
||||
->count('partner_id');
|
||||
if ($status) {
|
||||
$query->where('status', $status);
|
||||
}
|
||||
if ($paymentType) {
|
||||
$query->where('payment_type', $paymentType);
|
||||
}
|
||||
if ($partnerId) {
|
||||
$query->where('partner_id', $partnerId);
|
||||
}
|
||||
if ($managerUserId) {
|
||||
$query->where('manager_user_id', $managerUserId);
|
||||
}
|
||||
if ($search) {
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->whereHas('partner.user', fn ($uq) => $uq->where('name', 'like', "%{$search}%"))
|
||||
->orWhereHas('partner', fn ($pq) => $pq->where('partner_code', 'like', "%{$search}%"))
|
||||
->orWhereHas('management.tenant', fn ($tq) => $tq->where('company_name', 'like', "%{$search}%"));
|
||||
});
|
||||
}
|
||||
|
||||
$avgCommission = $totalPaidCount > 0 ? round($totalPaidAmount / $totalPaidCount) : 0;
|
||||
return $query;
|
||||
};
|
||||
|
||||
$statsCards = [
|
||||
'total_paid_amount' => $totalPaidAmount,
|
||||
'total_paid_count' => $totalPaidCount,
|
||||
$filters = compact('startYear', 'startMonth', 'endYear', 'endMonth', 'status', 'paymentType', 'partnerId', 'managerUserId', 'search');
|
||||
|
||||
// 통계 카드 8개
|
||||
$statsCards = $this->calculateStatsCards($baseQuery);
|
||||
|
||||
// 차트 데이터
|
||||
$monthlyTrend = $this->getMonthlyTrend($baseQuery, $startDate, $endDate);
|
||||
$typeRatio = $this->getTypeRatio($baseQuery);
|
||||
$topPartners = $this->getTopPartners($baseQuery);
|
||||
$statusDistribution = $this->getStatusDistribution($baseQuery);
|
||||
$monthlyComparison = $this->getMonthlyComparison($baseQuery, $startDate, $endDate);
|
||||
|
||||
// 테이블 데이터
|
||||
$partnerSettlement = $this->getPartnerSettlement($baseQuery);
|
||||
$managerSettlement = $this->getManagerSettlement($baseQuery);
|
||||
|
||||
// 필터 옵션
|
||||
$partners = SalesPartner::with('user')->active()->orderBy('partner_code')->get();
|
||||
$managers = User::whereIn('id', SalesCommission::whereNotNull('manager_user_id')->distinct()->pluck('manager_user_id'))->orderBy('name')->get();
|
||||
|
||||
return view('finance.settlement.payment-stats', compact(
|
||||
'filters', 'statsCards',
|
||||
'monthlyTrend', 'typeRatio', 'topPartners', 'statusDistribution', 'monthlyComparison',
|
||||
'partnerSettlement', 'managerSettlement',
|
||||
'partners', 'managers'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 카드 8개 계산
|
||||
*/
|
||||
private function calculateStatsCards(\Closure $baseQuery): array
|
||||
{
|
||||
$commissionSum = 'partner_commission + manager_commission + COALESCE(referrer_commission, 0)';
|
||||
|
||||
$totalAmount = (clone $baseQuery())->selectRaw("SUM({$commissionSum}) as total")->value('total') ?? 0;
|
||||
$totalCount = (clone $baseQuery())->count();
|
||||
$paidAmount = (clone $baseQuery())->where('status', SalesCommission::STATUS_PAID)
|
||||
->selectRaw("SUM({$commissionSum}) as total")->value('total') ?? 0;
|
||||
$unpaidAmount = (clone $baseQuery())->whereIn('status', [SalesCommission::STATUS_PENDING, SalesCommission::STATUS_APPROVED])
|
||||
->selectRaw("SUM({$commissionSum}) as total")->value('total') ?? 0;
|
||||
$activePartners = (clone $baseQuery())->distinct('partner_id')->count('partner_id');
|
||||
$partnerSum = (clone $baseQuery())->sum('partner_commission');
|
||||
$managerSum = (clone $baseQuery())->sum('manager_commission');
|
||||
$referrerSum = (clone $baseQuery())->selectRaw('SUM(COALESCE(referrer_commission, 0)) as total')->value('total') ?? 0;
|
||||
$avgCommission = $totalCount > 0 ? round($totalAmount / $totalCount) : 0;
|
||||
|
||||
return [
|
||||
'total_amount' => $totalAmount,
|
||||
'paid_amount' => $paidAmount,
|
||||
'unpaid_amount' => $unpaidAmount,
|
||||
'active_partners' => $activePartners,
|
||||
'partner_sum' => $partnerSum,
|
||||
'manager_sum' => $managerSum,
|
||||
'referrer_sum' => $referrerSum,
|
||||
'avg_commission' => $avgCommission,
|
||||
];
|
||||
}
|
||||
|
||||
// 차트 1 & 4: 월별 지급 추이 (해당 연도)
|
||||
$monthlyTrend = SalesCommission::where('status', SalesCommission::STATUS_PAID)
|
||||
->whereYear('actual_payment_date', $year)
|
||||
/**
|
||||
* 월별 지급 추이 (stacked bar)
|
||||
*/
|
||||
private function getMonthlyTrend(\Closure $baseQuery, Carbon $startDate, Carbon $endDate): \Illuminate\Support\Collection
|
||||
{
|
||||
return $baseQuery()
|
||||
->selectRaw("
|
||||
DATE_FORMAT(actual_payment_date, '%Y-%m') as month,
|
||||
DATE_FORMAT(scheduled_payment_date, '%Y-%m') as month,
|
||||
SUM(partner_commission) as partner_total,
|
||||
SUM(manager_commission) as manager_total,
|
||||
SUM(COALESCE(referrer_commission, 0)) as referrer_total,
|
||||
SUM(CASE WHEN status = 'paid' THEN partner_commission + manager_commission + COALESCE(referrer_commission, 0) ELSE 0 END) as paid_total,
|
||||
SUM(CASE WHEN status IN ('pending','approved') THEN partner_commission + manager_commission + COALESCE(referrer_commission, 0) ELSE 0 END) as unpaid_total,
|
||||
COUNT(*) as count
|
||||
")
|
||||
->groupByRaw("DATE_FORMAT(actual_payment_date, '%Y-%m')")
|
||||
->groupByRaw("DATE_FORMAT(scheduled_payment_date, '%Y-%m')")
|
||||
->orderBy('month')
|
||||
->get();
|
||||
}
|
||||
|
||||
// 차트 2: 수당 유형별 비율
|
||||
$typeRatio = SalesCommission::where('status', SalesCommission::STATUS_PAID)
|
||||
->whereYear('actual_payment_date', $year)
|
||||
/**
|
||||
* 수당 유형별 비율 (doughnut)
|
||||
*/
|
||||
private function getTypeRatio(\Closure $baseQuery): ?object
|
||||
{
|
||||
return $baseQuery()
|
||||
->selectRaw("
|
||||
SUM(partner_commission) as partner_total,
|
||||
SUM(manager_commission) as manager_total,
|
||||
SUM(COALESCE(referrer_commission, 0)) as referrer_total
|
||||
")
|
||||
->first();
|
||||
}
|
||||
|
||||
// 차트 3: 파트너별 수당 Top 10
|
||||
$topPartners = SalesCommission::where('status', SalesCommission::STATUS_PAID)
|
||||
->whereYear('actual_payment_date', $year)
|
||||
/**
|
||||
* 파트너별 Top 10 (horizontal bar)
|
||||
*/
|
||||
private function getTopPartners(\Closure $baseQuery): array
|
||||
{
|
||||
$topPartners = $baseQuery()
|
||||
->selectRaw('partner_id, SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total')
|
||||
->groupBy('partner_id')
|
||||
->orderByDesc('total')
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
$topPartnerNames = SalesPartner::with('user')
|
||||
$partnerNames = SalesPartner::with('user')
|
||||
->whereIn('id', $topPartners->pluck('partner_id'))
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
return view('finance.settlement.payment-stats', compact(
|
||||
'year', 'statsCards', 'monthlyTrend', 'typeRatio', 'topPartners', 'topPartnerNames'
|
||||
));
|
||||
return [
|
||||
'data' => $topPartners,
|
||||
'names' => $partnerNames->map(fn ($p) => $p->user?->name ?? $p->partner_code),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 상태별 건수/금액 분포 (doughnut + bar)
|
||||
*/
|
||||
private function getStatusDistribution(\Closure $baseQuery): \Illuminate\Support\Collection
|
||||
{
|
||||
return $baseQuery()
|
||||
->selectRaw("
|
||||
status,
|
||||
COUNT(*) as count,
|
||||
SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as amount
|
||||
")
|
||||
->groupBy('status')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 파트너 vs 매니저 월별 추이 (line) - paid만
|
||||
*/
|
||||
private function getMonthlyComparison(\Closure $baseQuery, Carbon $startDate, Carbon $endDate): \Illuminate\Support\Collection
|
||||
{
|
||||
return $baseQuery()
|
||||
->where('status', SalesCommission::STATUS_PAID)
|
||||
->selectRaw("
|
||||
DATE_FORMAT(actual_payment_date, '%Y-%m') as month,
|
||||
SUM(partner_commission) as partner_total,
|
||||
SUM(manager_commission) as manager_total
|
||||
")
|
||||
->groupByRaw("DATE_FORMAT(actual_payment_date, '%Y-%m')")
|
||||
->orderBy('month')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 파트너별 결산 테이블
|
||||
*/
|
||||
private function getPartnerSettlement(\Closure $baseQuery): \Illuminate\Support\Collection
|
||||
{
|
||||
$data = $baseQuery()
|
||||
->selectRaw("
|
||||
partner_id,
|
||||
COUNT(*) as contract_count,
|
||||
SUM(CASE WHEN payment_type = 'deposit' THEN partner_commission ELSE 0 END) as first_commission,
|
||||
SUM(CASE WHEN payment_type = 'balance' THEN partner_commission ELSE 0 END) as second_commission,
|
||||
SUM(partner_commission) as total_partner,
|
||||
SUM(CASE WHEN status = 'paid' THEN partner_commission + manager_commission + COALESCE(referrer_commission, 0) ELSE 0 END) as paid_amount,
|
||||
SUM(CASE WHEN status IN ('pending','approved') THEN partner_commission + manager_commission + COALESCE(referrer_commission, 0) ELSE 0 END) as unpaid_amount,
|
||||
SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total_amount
|
||||
")
|
||||
->groupBy('partner_id')
|
||||
->orderByDesc('total_amount')
|
||||
->get();
|
||||
|
||||
$partnerInfo = SalesPartner::with('user')
|
||||
->whereIn('id', $data->pluck('partner_id'))
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
return $data->map(function ($row) use ($partnerInfo) {
|
||||
$partner = $partnerInfo->get($row->partner_id);
|
||||
$row->partner_name = $partner?->user?->name ?? 'N/A';
|
||||
$row->partner_type = $partner?->partner_type ?? '';
|
||||
$row->completion_rate = $row->total_amount > 0 ? round(($row->paid_amount / $row->total_amount) * 100, 1) : 0;
|
||||
return $row;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 매니저별 결산 테이블
|
||||
*/
|
||||
private function getManagerSettlement(\Closure $baseQuery): \Illuminate\Support\Collection
|
||||
{
|
||||
$data = $baseQuery()
|
||||
->whereNotNull('manager_user_id')
|
||||
->selectRaw("
|
||||
manager_user_id,
|
||||
COUNT(*) as contract_count,
|
||||
SUM(manager_commission) as total_manager,
|
||||
SUM(CASE WHEN status = 'paid' THEN manager_commission ELSE 0 END) as paid_amount,
|
||||
SUM(CASE WHEN status IN ('pending','approved') THEN manager_commission ELSE 0 END) as unpaid_amount
|
||||
")
|
||||
->groupBy('manager_user_id')
|
||||
->orderByDesc('total_manager')
|
||||
->get();
|
||||
|
||||
$managerInfo = User::whereIn('id', $data->pluck('manager_user_id'))
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
return $data->map(function ($row) use ($managerInfo) {
|
||||
$manager = $managerInfo->get($row->manager_user_id);
|
||||
$row->manager_name = $manager?->name ?? 'N/A';
|
||||
$row->completion_rate = $row->total_manager > 0 ? round(($row->paid_amount / $row->total_manager) * 100, 1) : 0;
|
||||
return $row;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
@section('title', '수당지급현황통계')
|
||||
|
||||
@section('content')
|
||||
<div class="px-4 py-6">
|
||||
<div class="px-4 py-6" x-data="{ showFilters: false, activeTab: 'monthly' }">
|
||||
{{-- 페이지 헤더 --}}
|
||||
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center gap-4 mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800">수당지급현황통계</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ $year }}년 수당 지급 현황 종합 통계</p>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
{{ $filters['startYear'] }}년 {{ $filters['startMonth'] }}월 ~ {{ $filters['endYear'] }}년 {{ $filters['endMonth'] }}월 수당 종합 통계
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<a href="{{ route('finance.settlement', ['tab' => 'payment']) }}"
|
||||
@@ -18,79 +20,215 @@ class="inline-flex items-center gap-1 px-4 py-2 text-gray-600 bg-gray-100 hover:
|
||||
</svg>
|
||||
정산관리
|
||||
</a>
|
||||
<form method="GET" action="{{ route('finance.settlement.payment-stats') }}" class="flex items-center gap-2">
|
||||
<select name="year" class="border-gray-300 rounded-lg shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
@for ($y = now()->year; $y >= now()->year - 3; $y--)
|
||||
<option value="{{ $y }}" {{ $year == $y ? 'selected' : '' }}>{{ $y }}년</option>
|
||||
@endfor
|
||||
</select>
|
||||
<button type="submit" class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors text-sm">
|
||||
조회
|
||||
</button>
|
||||
</form>
|
||||
<button @click="showFilters = !showFilters"
|
||||
class="inline-flex items-center gap-1 px-4 py-2 text-sm rounded-lg transition-colors"
|
||||
:class="showFilters ? 'bg-indigo-600 text-white' : 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50'">
|
||||
<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="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"/>
|
||||
</svg>
|
||||
필터
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 통계 카드 --}}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-green-500">
|
||||
<div class="flex items-center justify-between">
|
||||
{{-- 필터 패널 --}}
|
||||
<div x-show="showFilters" x-collapse class="mb-6">
|
||||
<form method="GET" action="{{ route('finance.settlement.payment-stats') }}" class="bg-white rounded-lg shadow-sm p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
{{-- 시작 기간 --}}
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">총 지급액</p>
|
||||
<p class="text-xl font-bold text-green-600">{{ number_format($statsCards['total_paid_amount']) }}원</p>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">시작 기간</label>
|
||||
<div class="flex gap-2">
|
||||
<select name="start_year" class="flex-1 border-gray-300 rounded-lg shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
@for ($y = now()->year; $y >= now()->year - 3; $y--)
|
||||
<option value="{{ $y }}" {{ $filters['startYear'] == $y ? 'selected' : '' }}>{{ $y }}년</option>
|
||||
@endfor
|
||||
</select>
|
||||
<select name="start_month" class="flex-1 border-gray-300 rounded-lg shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
@for ($m = 1; $m <= 12; $m++)
|
||||
<option value="{{ $m }}" {{ $filters['startMonth'] == $m ? 'selected' : '' }}>{{ $m }}월</option>
|
||||
@endfor
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<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="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>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">종료 기간</label>
|
||||
<div class="flex gap-2">
|
||||
<select name="end_year" class="flex-1 border-gray-300 rounded-lg shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
@for ($y = now()->year; $y >= now()->year - 3; $y--)
|
||||
<option value="{{ $y }}" {{ $filters['endYear'] == $y ? 'selected' : '' }}>{{ $y }}년</option>
|
||||
@endfor
|
||||
</select>
|
||||
<select name="end_month" class="flex-1 border-gray-300 rounded-lg shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
@for ($m = 1; $m <= 12; $m++)
|
||||
<option value="{{ $m }}" {{ $filters['endMonth'] == $m ? 'selected' : '' }}>{{ $m }}월</option>
|
||||
@endfor
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{{-- 상태 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">상태</label>
|
||||
<select name="status" class="w-full border-gray-300 rounded-lg shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="">전체</option>
|
||||
<option value="pending" {{ $filters['status'] === 'pending' ? 'selected' : '' }}>대기</option>
|
||||
<option value="approved" {{ $filters['status'] === 'approved' ? 'selected' : '' }}>승인</option>
|
||||
<option value="paid" {{ $filters['status'] === 'paid' ? 'selected' : '' }}>지급완료</option>
|
||||
<option value="cancelled" {{ $filters['status'] === 'cancelled' ? 'selected' : '' }}>취소</option>
|
||||
</select>
|
||||
</div>
|
||||
{{-- 지급유형 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">지급유형</label>
|
||||
<select name="payment_type" class="w-full border-gray-300 rounded-lg shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="">전체</option>
|
||||
<option value="deposit" {{ $filters['paymentType'] === 'deposit' ? 'selected' : '' }}>계약금(1차)</option>
|
||||
<option value="balance" {{ $filters['paymentType'] === 'balance' ? 'selected' : '' }}>잔금(2차)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">{{ $year }}년 지급 합계</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{{-- 파트너 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">파트너</label>
|
||||
<select name="partner_id" class="w-full border-gray-300 rounded-lg shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="">전체</option>
|
||||
@foreach ($partners as $partner)
|
||||
<option value="{{ $partner->id }}" {{ $filters['partnerId'] == $partner->id ? 'selected' : '' }}>
|
||||
{{ $partner->user?->name ?? $partner->partner_code }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
{{-- 매니저 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">매니저</label>
|
||||
<select name="manager_user_id" class="w-full border-gray-300 rounded-lg shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<option value="">전체</option>
|
||||
@foreach ($managers as $manager)
|
||||
<option value="{{ $manager->id }}" {{ $filters['managerUserId'] == $manager->id ? 'selected' : '' }}>
|
||||
{{ $manager->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
{{-- 검색어 --}}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 mb-1">검색어</label>
|
||||
<input type="text" name="search" value="{{ $filters['search'] }}" placeholder="파트너명, 코드, 고객사..."
|
||||
class="w-full border-gray-300 rounded-lg shadow-sm text-sm focus:ring-indigo-500 focus:border-indigo-500">
|
||||
</div>
|
||||
{{-- 버튼 --}}
|
||||
<div class="flex items-end gap-2">
|
||||
<button type="submit" class="flex-1 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg transition-colors text-sm">
|
||||
조회
|
||||
</button>
|
||||
<a href="{{ route('finance.settlement.payment-stats') }}" class="px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors text-sm">
|
||||
초기화
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- 통계 카드 8개 --}}
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
{{-- 1. 총 수당 발생액 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-blue-500">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">총 지급 건수</p>
|
||||
<p class="text-xl font-bold text-blue-600">{{ number_format($statsCards['total_paid_count']) }}건</p>
|
||||
<p class="text-xs text-gray-500">총 수당 발생액</p>
|
||||
<p class="text-lg font-bold text-blue-600">{{ number_format($statsCards['total_amount']) }}<span class="text-xs font-normal">원</span></p>
|
||||
</div>
|
||||
<div class="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<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 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 2"/>
|
||||
</svg>
|
||||
<div class="w-9 h-9 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 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-xs text-gray-400 mt-1">{{ $year }}년 총 건수</p>
|
||||
</div>
|
||||
|
||||
{{-- 2. 지급 완료액 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-green-500">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">지급 완료액</p>
|
||||
<p class="text-lg font-bold text-green-600">{{ number_format($statsCards['paid_amount']) }}<span class="text-xs font-normal">원</span></p>
|
||||
</div>
|
||||
<div class="w-9 h-9 bg-green-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 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>
|
||||
</div>
|
||||
{{-- 3. 미지급액 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-orange-500">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">미지급액</p>
|
||||
<p class="text-lg font-bold text-orange-600">{{ number_format($statsCards['unpaid_amount']) }}<span class="text-xs font-normal">원</span></p>
|
||||
</div>
|
||||
<div class="w-9 h-9 bg-orange-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- 4. 활성 파트너 수 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-indigo-500">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">지급 파트너 수</p>
|
||||
<p class="text-xl font-bold text-indigo-600">{{ $statsCards['active_partners'] }}명</p>
|
||||
<p class="text-xs text-gray-500">활성 파트너 수</p>
|
||||
<p class="text-lg font-bold text-indigo-600">{{ $statsCards['active_partners'] }}<span class="text-xs font-normal">명</span></p>
|
||||
</div>
|
||||
<div class="w-10 h-10 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-indigo-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 0z"/>
|
||||
</svg>
|
||||
<div class="w-9 h-9 bg-indigo-100 rounded-full flex items-center justify-center">
|
||||
<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="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 0z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">{{ $year }}년 지급 대상</p>
|
||||
</div>
|
||||
|
||||
{{-- 5. 파트너 수당 합계 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-purple-500">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-500">건당 평균 수당</p>
|
||||
<p class="text-xl font-bold text-purple-600">{{ number_format($statsCards['avg_commission']) }}원</p>
|
||||
<p class="text-xs text-gray-500">파트너 수당</p>
|
||||
<p class="text-lg font-bold text-purple-600">{{ number_format($statsCards['partner_sum']) }}<span class="text-xs font-normal">원</span></p>
|
||||
</div>
|
||||
<div class="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<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="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/>
|
||||
</svg>
|
||||
<div class="w-9 h-9 bg-purple-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-purple-600" 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- 6. 매니저 수당 합계 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-teal-500">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">매니저 수당</p>
|
||||
<p class="text-lg font-bold text-teal-600">{{ number_format($statsCards['manager_sum']) }}<span class="text-xs font-normal">원</span></p>
|
||||
</div>
|
||||
<div class="w-9 h-9 bg-teal-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-teal-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- 7. 유치 수당 합계 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-amber-500">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">유치 수당</p>
|
||||
<p class="text-lg font-bold text-amber-600">{{ number_format($statsCards['referrer_sum']) }}<span class="text-xs font-normal">원</span></p>
|
||||
</div>
|
||||
<div class="w-9 h-9 bg-amber-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-amber-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>
|
||||
</div>
|
||||
{{-- 8. 건당 평균 수당 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 border-l-4 border-gray-400">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs text-gray-500">건당 평균 수당</p>
|
||||
<p class="text-lg font-bold text-gray-700">{{ number_format($statsCards['avg_commission']) }}<span class="text-xs font-normal">원</span></p>
|
||||
</div>
|
||||
<div class="w-9 h-9 bg-gray-100 rounded-full flex items-center justify-center">
|
||||
<svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-1">전체 평균</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +236,7 @@ class="inline-flex items-center gap-1 px-4 py-2 text-gray-600 bg-gray-100 hover:
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{{-- 차트 1: 월별 지급 추이 (Stacked Bar) --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">월별 지급 추이</h3>
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">월별 수당 추이</h3>
|
||||
<div class="relative" style="height: 300px;">
|
||||
<canvas id="monthlyTrendChart"></canvas>
|
||||
</div>
|
||||
@@ -120,21 +258,55 @@ class="inline-flex items-center gap-1 px-4 py-2 text-gray-600 bg-gray-100 hover:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 차트 4: 월별 지급 건수 추이 (Line) --}}
|
||||
{{-- 차트 4: 상태별 건수 분포 (Doughnut) --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">월별 지급 건수 추이</h3>
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">상태별 건수 분포</h3>
|
||||
<div class="relative" style="height: 300px;">
|
||||
<canvas id="monthlyCountChart"></canvas>
|
||||
<canvas id="statusCountChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 차트 5: 상태별 금액 분포 (Bar) --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">상태별 금액 분포</h3>
|
||||
<div class="relative" style="height: 300px;">
|
||||
<canvas id="statusAmountChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 차트 6: 파트너 vs 매니저 추이 (Line) --}}
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-4">파트너 vs 매니저 수당 추이 (지급완료)</h3>
|
||||
<div class="relative" style="height: 300px;">
|
||||
<canvas id="comparisonChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 월별 지급 요약 테이블 --}}
|
||||
{{-- 테이블 탭 --}}
|
||||
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-sm font-semibold text-gray-700">월별 지급 요약</h3>
|
||||
<div class="border-b border-gray-200">
|
||||
<nav class="flex -mb-px">
|
||||
<button @click="activeTab = 'monthly'"
|
||||
:class="activeTab === 'monthly' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
||||
class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
|
||||
월별 요약
|
||||
</button>
|
||||
<button @click="activeTab = 'partner'"
|
||||
:class="activeTab === 'partner' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
||||
class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
|
||||
파트너별 결산
|
||||
</button>
|
||||
<button @click="activeTab = 'manager'"
|
||||
:class="activeTab === 'manager' ? 'border-indigo-500 text-indigo-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
||||
class="px-6 py-3 border-b-2 text-sm font-medium transition-colors">
|
||||
매니저별 결산
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
|
||||
{{-- 월별 요약 테이블 --}}
|
||||
<div x-show="activeTab === 'monthly'" class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
@@ -142,42 +314,190 @@ class="inline-flex items-center gap-1 px-4 py-2 text-gray-600 bg-gray-100 hover:
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">파트너수당</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">매니저수당</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">유치수당</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">합계</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">지급완료</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">미지급</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">건수</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@php $grandPartner = 0; $grandManager = 0; $grandReferrer = 0; $grandTotal = 0; $grandCount = 0; @endphp
|
||||
@forelse ($monthlyTrend->reverse() as $row)
|
||||
@php $gPartner = 0; $gManager = 0; $gReferrer = 0; $gPaid = 0; $gUnpaid = 0; $gCount = 0; @endphp
|
||||
@forelse ($monthlyTrend as $row)
|
||||
@php
|
||||
$rowTotal = $row->partner_total + $row->manager_total + $row->referrer_total;
|
||||
$grandPartner += $row->partner_total;
|
||||
$grandManager += $row->manager_total;
|
||||
$grandReferrer += $row->referrer_total;
|
||||
$grandTotal += $rowTotal;
|
||||
$grandCount += $row->count;
|
||||
$gPartner += $row->partner_total;
|
||||
$gManager += $row->manager_total;
|
||||
$gReferrer += $row->referrer_total;
|
||||
$gPaid += $row->paid_total;
|
||||
$gUnpaid += $row->unpaid_total;
|
||||
$gCount += $row->count;
|
||||
@endphp
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ \Carbon\Carbon::parse($row->month . '-01')->format('n월') }}</td>
|
||||
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ \Carbon\Carbon::parse($row->month . '-01')->format('Y년 n월') }}</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($row->partner_total) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($row->manager_total) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($row->referrer_total) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right font-bold text-gray-900">{{ number_format($rowTotal) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-green-600 font-medium">{{ number_format($row->paid_total) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-orange-600 font-medium">{{ number_format($row->unpaid_total) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-center text-gray-900">{{ $row->count }}건</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-center text-gray-500">{{ $year }}년 지급 데이터가 없습니다.</td>
|
||||
<td colspan="7" class="px-4 py-8 text-center text-gray-500">해당 기간의 데이터가 없습니다.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
@if ($monthlyTrend->isNotEmpty())
|
||||
<tr class="bg-gray-50 font-bold">
|
||||
<td class="px-4 py-3 text-sm text-gray-900">합계</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($grandPartner) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($grandManager) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($grandReferrer) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-indigo-700">{{ number_format($grandTotal) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-center text-gray-900">{{ $grandCount }}건</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($gPartner) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($gManager) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($gReferrer) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-green-700">{{ number_format($gPaid) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-orange-700">{{ number_format($gUnpaid) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-center text-gray-900">{{ $gCount }}건</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- 파트너별 결산 테이블 --}}
|
||||
<div x-show="activeTab === 'partner'" 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">파트너</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">유형</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">건수</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">1차수당</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">2차수당</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">총수당</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">지급완료</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">미지급</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase" style="min-width:120px">완료율</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@php $pFirst = 0; $pSecond = 0; $pTotal = 0; $pPaid = 0; $pUnpaid = 0; $pCount = 0; @endphp
|
||||
@forelse ($partnerSettlement as $row)
|
||||
@php
|
||||
$pFirst += $row->first_commission;
|
||||
$pSecond += $row->second_commission;
|
||||
$pTotal += $row->total_amount;
|
||||
$pPaid += $row->paid_amount;
|
||||
$pUnpaid += $row->unpaid_amount;
|
||||
$pCount += $row->contract_count;
|
||||
@endphp
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ $row->partner_name }}</td>
|
||||
<td class="px-4 py-3 text-sm text-center">
|
||||
<span class="inline-flex px-2 py-0.5 text-xs rounded-full {{ $row->partner_type === 'corporate' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700' }}">
|
||||
{{ $row->partner_type === 'corporate' ? '법인' : '개인' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-center text-gray-900">{{ $row->contract_count }}건</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($row->first_commission) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($row->second_commission) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right font-bold text-gray-900">{{ number_format($row->total_amount) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-green-600 font-medium">{{ number_format($row->paid_amount) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-orange-600 font-medium">{{ number_format($row->unpaid_amount) }}원</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div class="h-full rounded-full {{ $row->completion_rate >= 80 ? 'bg-green-500' : ($row->completion_rate >= 50 ? 'bg-yellow-500' : 'bg-red-500') }}"
|
||||
style="width: {{ $row->completion_rate }}%"></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 w-10 text-right">{{ $row->completion_rate }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="9" class="px-4 py-8 text-center text-gray-500">해당 기간의 데이터가 없습니다.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
@if ($partnerSettlement->isNotEmpty())
|
||||
@php $overallRate = $pTotal > 0 ? round(($pPaid / $pTotal) * 100, 1) : 0; @endphp
|
||||
<tr class="bg-gray-50 font-bold">
|
||||
<td class="px-4 py-3 text-sm text-gray-900">합계</td>
|
||||
<td class="px-4 py-3 text-sm text-center text-gray-500">-</td>
|
||||
<td class="px-4 py-3 text-sm text-center text-gray-900">{{ $pCount }}건</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($pFirst) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($pSecond) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($pTotal) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-green-700">{{ number_format($pPaid) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-orange-700">{{ number_format($pUnpaid) }}원</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-indigo-500 rounded-full" style="width: {{ $overallRate }}%"></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 w-10 text-right">{{ $overallRate }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- 매니저별 결산 테이블 --}}
|
||||
<div x-show="activeTab === 'manager'" 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">매니저</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase">담당건수</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">매니저수당</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">지급완료</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">미지급</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase" style="min-width:120px">완료율</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@php $mTotal = 0; $mPaid = 0; $mUnpaid = 0; $mCount = 0; @endphp
|
||||
@forelse ($managerSettlement as $row)
|
||||
@php
|
||||
$mTotal += $row->total_manager;
|
||||
$mPaid += $row->paid_amount;
|
||||
$mUnpaid += $row->unpaid_amount;
|
||||
$mCount += $row->contract_count;
|
||||
@endphp
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 text-sm font-medium text-gray-900">{{ $row->manager_name }}</td>
|
||||
<td class="px-4 py-3 text-sm text-center text-gray-900">{{ $row->contract_count }}건</td>
|
||||
<td class="px-4 py-3 text-sm text-right font-bold text-gray-900">{{ number_format($row->total_manager) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-green-600 font-medium">{{ number_format($row->paid_amount) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-orange-600 font-medium">{{ number_format($row->unpaid_amount) }}원</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div class="h-full rounded-full {{ $row->completion_rate >= 80 ? 'bg-green-500' : ($row->completion_rate >= 50 ? 'bg-yellow-500' : 'bg-red-500') }}"
|
||||
style="width: {{ $row->completion_rate }}%"></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 w-10 text-right">{{ $row->completion_rate }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-center text-gray-500">해당 기간의 데이터가 없습니다.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
@if ($managerSettlement->isNotEmpty())
|
||||
@php $mOverallRate = $mTotal > 0 ? round(($mPaid / $mTotal) * 100, 1) : 0; @endphp
|
||||
<tr class="bg-gray-50 font-bold">
|
||||
<td class="px-4 py-3 text-sm text-gray-900">합계</td>
|
||||
<td class="px-4 py-3 text-sm text-center text-gray-900">{{ $mCount }}건</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-gray-900">{{ number_format($mTotal) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-green-700">{{ number_format($mPaid) }}원</td>
|
||||
<td class="px-4 py-3 text-sm text-right text-orange-700">{{ number_format($mUnpaid) }}원</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div class="h-full bg-indigo-500 rounded-full" style="width: {{ $mOverallRate }}%"></div>
|
||||
</div>
|
||||
<span class="text-xs text-gray-600 w-10 text-right">{{ $mOverallRate }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
@@ -205,49 +525,64 @@ class="inline-flex items-center gap-1 px-4 py-2 text-gray-600 bg-gray-100 hover:
|
||||
}
|
||||
};
|
||||
|
||||
// 월별 데이터
|
||||
const statusLabels = { pending: '대기', approved: '승인', paid: '지급완료', cancelled: '취소' };
|
||||
const statusColors = {
|
||||
pending: 'rgba(251, 191, 36, 0.8)',
|
||||
approved: 'rgba(59, 130, 246, 0.8)',
|
||||
paid: 'rgba(16, 185, 129, 0.8)',
|
||||
cancelled: 'rgba(239, 68, 68, 0.8)'
|
||||
};
|
||||
|
||||
// 빈 데이터 플러그인 (doughnut)
|
||||
const emptyDoughnutPlugin = {
|
||||
id: 'emptyDoughnut',
|
||||
afterDraw(chart) {
|
||||
const { datasets } = chart.data;
|
||||
if (chart.config.type !== 'doughnut') return;
|
||||
const hasData = datasets.some(ds => ds.data.some(v => v > 0));
|
||||
if (!hasData) {
|
||||
const { ctx, chartArea: { left, top, right, bottom } } = chart;
|
||||
const cx = (left + right) / 2;
|
||||
const cy = (top + bottom) / 2;
|
||||
ctx.save();
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = '14px sans-serif';
|
||||
ctx.fillStyle = '#9CA3AF';
|
||||
ctx.fillText('데이터 없음', cx, cy);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
};
|
||||
Chart.register(emptyDoughnutPlugin);
|
||||
|
||||
// === 차트 1: 월별 수당 추이 (Stacked Bar) ===
|
||||
const monthlyData = @json($monthlyTrend);
|
||||
const months = monthlyData.map(d => {
|
||||
const parts = d.month.split('-');
|
||||
return parseInt(parts[1]) + '월';
|
||||
});
|
||||
|
||||
// 차트 1: 월별 지급 추이 (Stacked Bar)
|
||||
new Chart(document.getElementById('monthlyTrendChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: months,
|
||||
datasets: [
|
||||
{
|
||||
label: '파트너수당',
|
||||
data: monthlyData.map(d => Number(d.partner_total)),
|
||||
backgroundColor: 'rgba(79, 70, 229, 0.8)',
|
||||
},
|
||||
{
|
||||
label: '매니저수당',
|
||||
data: monthlyData.map(d => Number(d.manager_total)),
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.8)',
|
||||
},
|
||||
{
|
||||
label: '유치수당',
|
||||
data: monthlyData.map(d => Number(d.referrer_total)),
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.8)',
|
||||
}
|
||||
{ label: '파트너수당', data: monthlyData.map(d => Number(d.partner_total)), backgroundColor: 'rgba(79, 70, 229, 0.8)' },
|
||||
{ label: '매니저수당', data: monthlyData.map(d => Number(d.manager_total)), backgroundColor: 'rgba(16, 185, 129, 0.8)' },
|
||||
{ label: '유치수당', data: monthlyData.map(d => Number(d.referrer_total)), backgroundColor: 'rgba(245, 158, 11, 0.8)' }
|
||||
]
|
||||
},
|
||||
options: {
|
||||
...defaultOptions,
|
||||
scales: {
|
||||
x: { stacked: true },
|
||||
y: {
|
||||
stacked: true,
|
||||
ticks: { callback: val => formatCurrency(val) }
|
||||
}
|
||||
y: { stacked: true, ticks: { callback: val => formatCurrency(val) } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 차트 2: 수당 유형별 비율 (Doughnut)
|
||||
// === 차트 2: 수당 유형별 비율 (Doughnut) ===
|
||||
const typeData = @json($typeRatio);
|
||||
new Chart(document.getElementById('typeRatioChart'), {
|
||||
type: 'doughnut',
|
||||
@@ -259,13 +594,8 @@ class="inline-flex items-center gap-1 px-4 py-2 text-gray-600 bg-gray-100 hover:
|
||||
Number(typeData?.manager_total || 0),
|
||||
Number(typeData?.referrer_total || 0)
|
||||
],
|
||||
backgroundColor: [
|
||||
'rgba(79, 70, 229, 0.8)',
|
||||
'rgba(16, 185, 129, 0.8)',
|
||||
'rgba(245, 158, 11, 0.8)'
|
||||
],
|
||||
borderWidth: 2,
|
||||
borderColor: '#fff'
|
||||
backgroundColor: ['rgba(79, 70, 229, 0.8)', 'rgba(16, 185, 129, 0.8)', 'rgba(245, 158, 11, 0.8)'],
|
||||
borderWidth: 2, borderColor: '#fff'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
@@ -285,9 +615,9 @@ class="inline-flex items-center gap-1 px-4 py-2 text-gray-600 bg-gray-100 hover:
|
||||
}
|
||||
});
|
||||
|
||||
// 차트 3: 파트너별 수당 Top 10 (Horizontal Bar)
|
||||
const topPartnersData = @json($topPartners);
|
||||
const topPartnerNames = @json($topPartnerNames->map(fn ($p) => $p->user?->name ?? $p->partner_code));
|
||||
// === 차트 3: 파트너별 수당 Top 10 (Horizontal Bar) ===
|
||||
const topPartnersData = @json($topPartners['data']);
|
||||
const topPartnerNames = @json($topPartners['names']);
|
||||
new Chart(document.getElementById('topPartnersChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
@@ -303,47 +633,92 @@ class="inline-flex items-center gap-1 px-4 py-2 text-gray-600 bg-gray-100 hover:
|
||||
options: {
|
||||
...defaultOptions,
|
||||
indexAxis: 'y',
|
||||
scales: {
|
||||
x: { ticks: { callback: val => formatCurrency(val) } }
|
||||
},
|
||||
plugins: {
|
||||
...defaultOptions.plugins,
|
||||
legend: { display: false }
|
||||
}
|
||||
scales: { x: { ticks: { callback: val => formatCurrency(val) } } },
|
||||
plugins: { ...defaultOptions.plugins, legend: { display: false } }
|
||||
}
|
||||
});
|
||||
|
||||
// 차트 4: 월별 지급 건수 추이 (Line)
|
||||
new Chart(document.getElementById('monthlyCountChart'), {
|
||||
type: 'line',
|
||||
// === 차트 4: 상태별 건수 분포 (Doughnut) ===
|
||||
const statusData = @json($statusDistribution);
|
||||
new Chart(document.getElementById('statusCountChart'), {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: months,
|
||||
labels: statusData.map(d => statusLabels[d.status] || d.status),
|
||||
datasets: [{
|
||||
label: '지급 건수',
|
||||
data: monthlyData.map(d => Number(d.count)),
|
||||
borderColor: 'rgba(79, 70, 229, 1)',
|
||||
backgroundColor: 'rgba(79, 70, 229, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
pointBackgroundColor: 'rgba(79, 70, 229, 1)',
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 6
|
||||
data: statusData.map(d => Number(d.count)),
|
||||
backgroundColor: statusData.map(d => statusColors[d.status] || 'rgba(156,163,175,0.8)'),
|
||||
borderWidth: 2, borderColor: '#fff'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...defaultOptions,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { stepSize: 1 }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
...defaultOptions.plugins,
|
||||
legend: { display: false }
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(ctx) {
|
||||
const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const pct = total > 0 ? ((ctx.raw / total) * 100).toFixed(1) : 0;
|
||||
return ctx.label + ': ' + ctx.raw + '건 (' + pct + '%)';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// === 차트 5: 상태별 금액 분포 (Bar) ===
|
||||
new Chart(document.getElementById('statusAmountChart'), {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: statusData.map(d => statusLabels[d.status] || d.status),
|
||||
datasets: [{
|
||||
label: '금액',
|
||||
data: statusData.map(d => Number(d.amount)),
|
||||
backgroundColor: statusData.map(d => statusColors[d.status] || 'rgba(156,163,175,0.8)'),
|
||||
borderWidth: 1,
|
||||
borderColor: statusData.map(d => (statusColors[d.status] || 'rgba(156,163,175,1)').replace('0.8', '1'))
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
...defaultOptions,
|
||||
scales: { y: { ticks: { callback: val => formatCurrency(val) } } },
|
||||
plugins: { ...defaultOptions.plugins, legend: { display: false } }
|
||||
}
|
||||
});
|
||||
|
||||
// === 차트 6: 파트너 vs 매니저 추이 (Line) ===
|
||||
const comparisonData = @json($monthlyComparison);
|
||||
const compMonths = comparisonData.map(d => {
|
||||
const parts = d.month.split('-');
|
||||
return parseInt(parts[1]) + '월';
|
||||
});
|
||||
new Chart(document.getElementById('comparisonChart'), {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: compMonths,
|
||||
datasets: [
|
||||
{
|
||||
label: '파트너수당',
|
||||
data: comparisonData.map(d => Number(d.partner_total)),
|
||||
borderColor: 'rgba(79, 70, 229, 1)',
|
||||
backgroundColor: 'rgba(79, 70, 229, 0.1)',
|
||||
fill: true, tension: 0.3, pointRadius: 4, pointHoverRadius: 6
|
||||
},
|
||||
{
|
||||
label: '매니저수당',
|
||||
data: comparisonData.map(d => Number(d.manager_total)),
|
||||
borderColor: 'rgba(16, 185, 129, 1)',
|
||||
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
||||
fill: true, tension: 0.3, pointRadius: 4, pointHoverRadius: 6
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
...defaultOptions,
|
||||
scales: { y: { ticks: { callback: val => formatCurrency(val) } } }
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
Reference in New Issue
Block a user