header('HX-Request') && !$request->header('HX-Boosted')) { return response('', 200)->header('HX-Redirect', route('finance.settlement')); } $initialTab = $request->input('tab', 'commission'); // 수당 정산 탭 데이터 (기본 탭이므로 즉시 로드) $year = $request->input('year', now()->year); $month = $request->input('month', now()->month); $dateRange = $request->boolean('date_range'); $filters = [ 'status' => $request->input('status'), 'payment_type' => $request->input('payment_type'), 'partner_id' => $request->input('partner_id'), 'commission_type' => $request->input('commission_type'), 'search' => $request->input('search'), 'date_range' => $dateRange, ]; if ($dateRange) { $filters['scheduled_start_year'] = (int) $request->input('start_year', $year); $filters['scheduled_start_month'] = (int) $request->input('start_month', $month); $filters['scheduled_end_year'] = (int) $request->input('end_year', $year); $filters['scheduled_end_month'] = (int) $request->input('end_month', $month); $filters['start_year'] = $filters['scheduled_start_year']; $filters['start_month'] = $filters['scheduled_start_month']; $filters['end_year'] = $filters['scheduled_end_year']; $filters['end_month'] = $filters['scheduled_end_month']; } else { $filters['scheduled_year'] = $year; $filters['scheduled_month'] = $month; } $commissions = $this->service->getCommissions($filters); $stats = $dateRange ? $this->service->getSettlementStatsForRange( $filters['scheduled_start_year'], $filters['scheduled_start_month'], $filters['scheduled_end_year'], $filters['scheduled_end_month'] ) : $this->service->getSettlementStats($year, $month); $partners = SalesPartner::with('user') ->active() ->orderBy('partner_code') ->get(); $pendingTenants = $this->service->getPendingPaymentTenants(); // 통합 통계 (페이지 상단) $summaryStats = $this->getSummaryStats(); return view('finance.settlement.index', compact( 'initialTab', 'commissions', 'stats', 'partners', 'pendingTenants', 'year', 'month', 'filters', 'summaryStats' )); } /** * 수당 통계카드 HTMX 갱신 */ public function commissionStats(Request $request): View { $year = $request->input('year', now()->year); $month = $request->input('month', now()->month); $stats = $this->service->getSettlementStats($year, $month); return view('finance.settlement.partials.commission.stats-cards', compact('stats', 'year', 'month')); } /** * 수당 테이블 HTMX 갱신 */ public function commissionTable(Request $request): View { $year = $request->input('year', now()->year); $month = $request->input('month', now()->month); $dateRange = $request->boolean('date_range'); $filters = [ 'status' => $request->input('status'), 'payment_type' => $request->input('payment_type'), 'partner_id' => $request->input('partner_id'), 'commission_type' => $request->input('commission_type'), 'search' => $request->input('search'), ]; if ($dateRange) { $filters['scheduled_start_year'] = (int) $request->input('start_year', $year); $filters['scheduled_start_month'] = (int) $request->input('start_month', $month); $filters['scheduled_end_year'] = (int) $request->input('end_year', $year); $filters['scheduled_end_month'] = (int) $request->input('end_month', $month); } else { $filters['scheduled_year'] = $year; $filters['scheduled_month'] = $month; } $commissions = $this->service->getCommissions($filters); return view('finance.settlement.partials.commission.table', compact('commissions')); } /** * 파트너별 현황 탭 */ public function partnerSummary(Request $request): View { $query = SalesPartner::with('user'); // 검색 if ($search = $request->input('search')) { $query->where(function ($q) use ($search) { $q->where('partner_code', 'like', "%{$search}%") ->orWhereHas('user', function ($uq) use ($search) { $uq->where('name', 'like', "%{$search}%"); }); }); } // 유형 필터 if ($type = $request->input('type')) { if ($type === 'individual') { $query->where('partner_type', '!=', 'corporate'); } elseif ($type === 'corporate') { $query->where('partner_type', 'corporate'); } } // 상태 필터 if ($request->input('status', 'active') === 'active') { $query->active(); } $partners = $query->orderBy('partner_code')->paginate(20); // 각 파트너별 수당 집계 $partnerIds = $partners->pluck('id')->toArray(); if (!empty($partnerIds)) { $commissionStats = SalesCommission::selectRaw(' partner_id, SUM(CASE WHEN status = "paid" THEN partner_commission ELSE 0 END) as paid_total, SUM(CASE WHEN status IN ("pending", "approved") THEN partner_commission ELSE 0 END) as unpaid_total, COUNT(*) as total_count, MAX(CASE WHEN status = "paid" THEN actual_payment_date ELSE NULL END) as last_paid_date ') ->whereIn('partner_id', $partnerIds) ->groupBy('partner_id') ->get() ->keyBy('partner_id'); } else { $commissionStats = collect(); } return view('finance.settlement.partials.partner-summary', compact('partners', 'commissionStats')); } /** * 컨설팅비용 탭 */ public function consultingTab(Request $request): View { return view('finance.settlement.partials.consulting-tab'); } /** * 고객사정산 탭 */ public function customerTab(Request $request): View { $query = SalesTenantManagement::with([ 'tenant', 'tenantProspect.registeredBy.salesPartner', 'salesPartner.user', 'manager', 'commissions', 'contractProducts', ])->where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_PENDING); // 필터: 검색 (회사명) if ($search = $request->input('search')) { $query->where(function ($q) use ($search) { $q->whereHas('tenant', fn ($tq) => $tq->where('company_name', 'like', "%{$search}%")) ->orWhereHas('tenantProspect', fn ($pq) => $pq->where('company_name', 'like', "%{$search}%")); }); } // 필터: 개발 상태 if ($hqStatus = $request->input('hq_status')) { $query->where('hq_status', $hqStatus); } // 필터: 담당 파트너 (salesPartner 또는 tenantProspect.registeredBy.salesPartner) if ($partnerId = $request->input('partner_id')) { $query->where(function ($q) use ($partnerId) { $q->where('sales_partner_id', $partnerId) ->orWhereHas('tenantProspect.registeredBy.salesPartner', function ($sq) use ($partnerId) { $sq->where('id', $partnerId); }); }); } // 필터: 수금 상태 $paymentStatus = $request->input('payment_status'); $managements = $query->orderByDesc('id')->paginate(20)->withQueryString(); // 수금 상태 필터 (컬렉션 레벨) if ($paymentStatus) { $managements->setCollection( $managements->getCollection()->filter(function ($mgmt) use ($paymentStatus) { $depositPaid = $mgmt->deposit_status === 'paid'; $balancePaid = $mgmt->balance_status === 'paid'; return match ($paymentStatus) { 'fully_paid' => $depositPaid && $balancePaid, 'partial' => ($depositPaid || $balancePaid) && !($depositPaid && $balancePaid), 'unpaid' => !$depositPaid && !$balancePaid, default => true, }; }) ); } // 구독료 일괄 조회 (N+1 방지) $tenantIds = $managements->getCollection() ->pluck('tenant_id') ->filter() ->unique() ->values() ->toArray(); $subscriptionFees = []; if (!empty($tenantIds)) { $subscriptionFees = SalesContractProduct::whereIn('tenant_id', $tenantIds) ->selectRaw('tenant_id, SUM(subscription_fee) as total_subscription_fee') ->groupBy('tenant_id') ->pluck('total_subscription_fee', 'tenant_id') ->toArray(); } // 파트너 목록 (필터용) $partners = SalesPartner::with('user') ->active() ->orderBy('partner_code') ->get(); // hqStatusLabels에서 pending 제외 $hqStatusLabels = collect(SalesTenantManagement::$hqStatusLabels) ->except(SalesTenantManagement::HQ_STATUS_PENDING) ->toArray(); // 통계 카드 $customerStats = $this->getCustomerStats(); return view('finance.settlement.partials.customer-tab', compact( 'managements', 'subscriptionFees', 'partners', 'hqStatusLabels', 'customerStats', )); } /** * 구독관리 탭 */ public function subscriptionTab(Request $request): View { // 인계(handover) 완료된 업체 = 구독 업체 $query = SalesTenantManagement::with([ 'tenant', 'tenantProspect', 'salesPartner.user', 'manager', 'contractProducts.product', 'contractProducts.category', ]) ->where('hq_status', 'handover'); // 검색 필터 if ($search = $request->input('search')) { $query->where(function ($q) use ($search) { $q->whereHas('tenant', fn($t) => $t->where('company_name', 'like', "%{$search}%")) ->orWhereHas('tenantProspect', fn($t) => $t->where('company_name', 'like', "%{$search}%")); }); } $managements = $query->orderBy('contracted_at', 'desc')->get(); // 통계 계산 $stats = [ 'activeCount' => $managements->count(), 'monthlyRecurring' => $managements->sum(fn($m) => $m->contractProducts->sum('subscription_fee')), 'totalProducts' => $managements->sum(fn($m) => $m->contractProducts->where('subscription_fee', '>', 0)->count()), ]; $stats['yearlyRecurring'] = $stats['monthlyRecurring'] * 12; return view('finance.settlement.partials.subscription-tab', compact('managements', 'stats')); } /** * 수당 지급 탭 (파트너별 그룹핑) */ public function paymentTab(Request $request): View { // approved 상태 수당을 partner_id 기준 GROUP BY $partnerPayments = SalesCommission::where('status', SalesCommission::STATUS_APPROVED) ->selectRaw(' partner_id, GROUP_CONCAT(id) as commission_ids, COUNT(*) as count, SUM(partner_commission) as partner_total, SUM(manager_commission) as manager_total, SUM(COALESCE(referrer_commission, 0)) as referrer_total ') ->groupBy('partner_id') ->get(); // 파트너 정보 eager load $partners = SalesPartner::with('user') ->whereIn('id', $partnerPayments->pluck('partner_id')) ->get() ->keyBy('id'); // 통계 카드 데이터 $now = now(); $paymentStats = [ 'waiting_count' => $partnerPayments->sum('count'), 'waiting_amount' => $partnerPayments->sum(fn ($p) => $p->partner_total + $p->manager_total + $p->referrer_total), 'this_month_paid_count' => SalesCommission::where('status', SalesCommission::STATUS_PAID) ->whereYear('actual_payment_date', $now->year) ->whereMonth('actual_payment_date', $now->month) ->count(), 'this_month_paid_amount' => SalesCommission::where('status', SalesCommission::STATUS_PAID) ->whereYear('actual_payment_date', $now->year) ->whereMonth('actual_payment_date', $now->month) ->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total') ->value('total') ?? 0, 'partner_total' => $partnerPayments->sum('partner_total'), 'manager_referrer_total' => $partnerPayments->sum('manager_total') + $partnerPayments->sum('referrer_total'), ]; return view('finance.settlement.partials.payment-tab', compact('partnerPayments', 'partners', 'paymentStats')); } /** * 파트너별 수당 건 상세 (HTMX partial) */ public function paymentPartnerDetail(int $partnerId): View { $commissions = SalesCommission::where('status', SalesCommission::STATUS_APPROVED) ->where('partner_id', $partnerId) ->with(['management.tenant', 'manager']) ->orderBy('scheduled_payment_date') ->get(); return view('finance.settlement.partials.payment-partner-detail', compact('commissions', 'partnerId')); } /** * 수당지급현황통계 페이지 */ public function paymentStats(Request $request): View|Response { if ($request->header('HX-Request') && !$request->header('HX-Boosted')) { return response('', 200)->header('HX-Redirect', route('finance.settlement.payment-stats')); } // 필터 파라미터 $now = now(); $dateRange = $request->boolean('date_range'); if ($dateRange) { // 기간 범위 모드 $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); } else { // 단일 월 모드 (기본) $singleYear = (int) $request->input('year', $now->year); $singleMonth = (int) $request->input('month', $now->month); $startYear = $singleYear; $startMonth = $singleMonth; $endYear = $singleYear; $endMonth = $singleMonth; } $status = $request->input('status'); $paymentType = $request->input('payment_type'); $partnerId = $request->input('partner_id'); $managerUserId = $request->input('manager_user_id'); $search = $request->input('search'); $startDate = Carbon::create($startYear, $startMonth, 1)->startOfMonth(); $endDate = Carbon::create($endYear, $endMonth, 1)->endOfMonth(); // 공통 baseQuery 클로저 $baseQuery = function () use ($startDate, $endDate, $status, $paymentType, $partnerId, $managerUserId, $search) { $query = SalesCommission::query() ->whereBetween('scheduled_payment_date', [$startDate, $endDate]); 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}%")); }); } return $query; }; $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, ]; } /** * 월별 지급 추이 (stacked bar) */ private function getMonthlyTrend(\Closure $baseQuery, Carbon $startDate, Carbon $endDate): \Illuminate\Support\Collection { return $baseQuery() ->selectRaw(" 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(scheduled_payment_date, '%Y-%m')") ->orderBy('month') ->get(); } /** * 수당 유형별 비율 (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(); } /** * 파트너별 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(); $partnerNames = SalesPartner::with('user') ->whereIn('id', $topPartners->pluck('partner_id')) ->get() ->keyBy('id'); 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; }); } /** * 고객사정산 통계 데이터 */ private function getCustomerStats(): array { $baseQuery = SalesTenantManagement::where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_PENDING); // 총 개발비 $totalFee = (clone $baseQuery)->sum('total_registration_fee'); $totalCount = (clone $baseQuery)->count(); // 수금완료 (deposit + balance 모두 paid인 건의 합계) $collectedAmount = (clone $baseQuery) ->where('deposit_status', 'paid') ->sum('deposit_amount') + (clone $baseQuery) ->where('balance_status', 'paid') ->sum('balance_amount'); // 미수금 $uncollectedAmount = $totalFee - $collectedAmount; // 개발 진행 중 (handover 제외) $inProgressCount = (clone $baseQuery) ->where('hq_status', '!=', SalesTenantManagement::HQ_STATUS_HANDOVER) ->count(); // 구독 전환 (handover + tenant active) $subscriptionCount = SalesTenantManagement::where('hq_status', SalesTenantManagement::HQ_STATUS_HANDOVER) ->whereHas('tenant', fn ($q) => $q->whereNull('deleted_at')) ->count(); return [ 'total_fee' => $totalFee, 'total_count' => $totalCount, 'collected_amount' => $collectedAmount, 'uncollected_amount' => max(0, $uncollectedAmount), 'in_progress_count' => $inProgressCount, 'subscription_count' => $subscriptionCount, ]; } /** * 통합 통계 데이터 */ private function getSummaryStats(): array { $now = now(); // 미지급 수당 (pending + approved) $unpaidAmount = SalesCommission::whereIn('status', [ SalesCommission::STATUS_PENDING, SalesCommission::STATUS_APPROVED, ])->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total') ->value('total') ?? 0; // 승인 대기 건수 $pendingCount = SalesCommission::where('status', SalesCommission::STATUS_PENDING)->count(); // 이번달 지급예정 $thisMonthScheduled = SalesCommission::whereIn('status', [ SalesCommission::STATUS_PENDING, SalesCommission::STATUS_APPROVED, ]) ->whereYear('scheduled_payment_date', $now->year) ->whereMonth('scheduled_payment_date', $now->month) ->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total') ->value('total') ?? 0; // 누적 지급완료 $totalPaid = SalesCommission::where('status', SalesCommission::STATUS_PAID) ->selectRaw('SUM(partner_commission + manager_commission + COALESCE(referrer_commission, 0)) as total') ->value('total') ?? 0; return [ 'unpaid_amount' => $unpaidAmount, 'pending_count' => $pendingCount, 'this_month_scheduled' => $thisMonthScheduled, 'total_paid' => $totalPaid, ]; } }