- 파트너별 GROUP BY 요약 → 개별 commission 레코드 표시로 변경 - 1차/2차수당 계산에 매니저/유치 수당 포함하여 합계 불일치 해소 - 파트너 헤더행 + 개별 건 행 + 합계행 구조로 테이블 재구성
753 lines
29 KiB
PHP
753 lines
29 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Finance;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Sales\SalesCommission;
|
|
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\View\View;
|
|
|
|
class SettlementController extends Controller
|
|
{
|
|
public function __construct(
|
|
private SalesCommissionService $service
|
|
) {}
|
|
|
|
/**
|
|
* 통합 정산관리 메인 페이지
|
|
*/
|
|
public function index(Request $request): View|Response
|
|
{
|
|
if ($request->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()
|
|
->with(['partner.user', 'management.tenant'])
|
|
->select([
|
|
'id', 'partner_id', 'management_id', 'payment_type',
|
|
'partner_commission', 'manager_commission', 'referrer_commission',
|
|
'status', 'scheduled_payment_date',
|
|
])
|
|
->orderBy('partner_id')
|
|
->orderBy('scheduled_payment_date')
|
|
->get();
|
|
|
|
$commissionTotal = fn ($item) => $item->partner_commission + $item->manager_commission + ($item->referrer_commission ?? 0);
|
|
|
|
return $data->groupBy('partner_id')->map(function ($items, $partnerId) use ($commissionTotal) {
|
|
$partner = $items->first()->partner;
|
|
|
|
return (object) [
|
|
'partner_id' => $partnerId,
|
|
'partner_name' => $partner?->user?->name ?? 'N/A',
|
|
'partner_type' => $partner?->partner_type ?? '',
|
|
'items' => $items,
|
|
'contract_count' => $items->count(),
|
|
'first_commission' => $items->where('payment_type', 'deposit')->sum($commissionTotal),
|
|
'second_commission' => $items->where('payment_type', 'balance')->sum($commissionTotal),
|
|
'total_amount' => $items->sum($commissionTotal),
|
|
'paid_amount' => $items->where('status', 'paid')->sum($commissionTotal),
|
|
'unpaid_amount' => $items->whereIn('status', ['pending', 'approved'])->sum($commissionTotal),
|
|
];
|
|
})->sortByDesc('total_amount')->values();
|
|
}
|
|
|
|
/**
|
|
* 매니저별 결산 테이블
|
|
*/
|
|
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,
|
|
];
|
|
}
|
|
}
|