Files
sam-manage/app/Http/Controllers/Sales/SalesDashboardController.php
김보곤 3ab9d20376 feat:유치 파트너 예상수당 계산 개선
- 가입비 설정 시: 가입비 × 5% 예상수당 표시
- 가입비 미설정 시: "계약전" 표시
- 요약 통계 및 파트너별 예상수당 모두 적용

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 09:49:28 +09:00

604 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers\Sales;
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\Sales\TenantProspect;
use App\Models\Tenants\Tenant;
use App\Models\User;
use App\Services\SalesCommissionService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\View\View;
/**
* 영업관리 대시보드 컨트롤러
*/
class SalesDashboardController extends Controller
{
public function __construct(
private SalesCommissionService $commissionService
) {}
/**
* 대시보드 화면
*/
public function index(Request $request): View
{
$data = $this->getDashboardData($request);
// 영업파트너 수당 정보 추가
$data = array_merge($data, $this->getCommissionData());
return view('sales.dashboard.index', $data);
}
/**
* HTMX 부분 새로고침용 데이터 반환
*/
public function refresh(Request $request): View
{
$data = $this->getDashboardData($request);
return view('sales.dashboard.partials.data-container', $data);
}
/**
* 대시보드 데이터 조회
*/
private function getDashboardData(Request $request): array
{
// 기간 설정
$period = $request->input('period', 'month'); // month or custom
$year = $request->input('year', now()->year);
$month = $request->input('month', now()->month);
// 기간 설정 모드일 경우
if ($period === 'custom') {
$startDate = $request->input('start_date', now()->startOfMonth()->format('Y-m-d'));
$endDate = $request->input('end_date', now()->format('Y-m-d'));
} else {
$startDate = now()->startOfMonth()->format('Y-m-d');
$endDate = now()->endOfMonth()->format('Y-m-d');
}
$currentUserId = auth()->id();
$childrenIds = auth()->user()->children()->pluck('id')->toArray();
$partnerIds = array_merge([$currentUserId], $childrenIds);
// 현재 사용자의 영업파트너 정보 조회
$partner = SalesPartner::where('user_id', $currentUserId)->first();
$partnerId = $partner?->id;
// 나와 관련된 모든 수당 조회 (영업파트너로서 + 매니저로서)
$myCommissionsAsPartner = $partnerId
? SalesCommission::forPartner($partnerId)->get()
: collect();
$myCommissionsAsManager = SalesCommission::forManager($currentUserId)->get();
// 판매자(영업파트너) 수당 계산
$partnerCommissionTotal = $myCommissionsAsPartner->sum('partner_commission');
$partnerCommissionPaid = $myCommissionsAsPartner->where('status', SalesCommission::STATUS_PAID)->sum('partner_commission');
$partnerCommissionPending = $myCommissionsAsPartner->where('status', SalesCommission::STATUS_PENDING)->sum('partner_commission');
$partnerCommissionApproved = $myCommissionsAsPartner->where('status', SalesCommission::STATUS_APPROVED)->sum('partner_commission');
// 매니저 수당 계산
$managerCommissionTotal = $myCommissionsAsManager->sum('manager_commission');
$managerCommissionPaid = $myCommissionsAsManager->where('status', SalesCommission::STATUS_PAID)->sum('manager_commission');
$managerCommissionPending = $myCommissionsAsManager->where('status', SalesCommission::STATUS_PENDING)->sum('manager_commission');
$managerCommissionApproved = $myCommissionsAsManager->where('status', SalesCommission::STATUS_APPROVED)->sum('manager_commission');
// 총 수당 계산 (중복 제거: 동일 commission에서 partner + manager인 경우)
$allCommissionIds = $myCommissionsAsPartner->pluck('id')->merge($myCommissionsAsManager->pluck('id'))->unique();
$totalContracts = $allCommissionIds->count();
// 통계 데이터 (실제 데이터)
$totalMembershipFee = $myCommissionsAsPartner->sum('payment_amount') + $myCommissionsAsManager->sum('payment_amount');
$totalCommission = $partnerCommissionTotal + $managerCommissionTotal;
$paidCommission = $partnerCommissionPaid + $managerCommissionPaid;
$commissionRate = $totalCommission > 0 ? round(($paidCommission / $totalCommission) * 100, 1) : 0;
$stats = [
'total_membership_fee' => $totalMembershipFee, // 총 가입비
'total_commission' => $totalCommission, // 총 수당
'commission_rate' => $commissionRate, // 지급 완료 비율
'total_contracts' => $totalContracts, // 전체 건수
'pending_membership_approval' => $myCommissionsAsPartner->where('status', SalesCommission::STATUS_PENDING)->count()
+ $myCommissionsAsManager->where('status', SalesCommission::STATUS_PENDING)->count(),
'pending_payment_approval' => $myCommissionsAsPartner->where('status', SalesCommission::STATUS_APPROVED)->count()
+ $myCommissionsAsManager->where('status', SalesCommission::STATUS_APPROVED)->count(),
];
// 역할별 수당 상세 (실제 데이터)
$commissionByRole = [
[
'name' => '판매자',
'rate' => 20,
'amount' => $partnerCommissionTotal,
'paid' => $partnerCommissionPaid,
'pending' => $partnerCommissionPending,
'approved' => $partnerCommissionApproved,
'color' => 'green',
],
[
'name' => '관리자',
'rate' => 5,
'amount' => $managerCommissionTotal,
'paid' => $managerCommissionPaid,
'pending' => $managerCommissionPending,
'approved' => $managerCommissionApproved,
'color' => 'blue',
],
[
'name' => '협업지원금',
'rate' => null, // 메뉴당 2,000원
'amount' => null, // 가입비 완납 시 계산
'color' => 'purple',
],
];
// === 인계(handover) 완료된 가망고객의 수당 계산 ===
// 내가 등록한 가망고객 중 인계 완료된 것들의 계약 금액 조회
$handoverProspectIds = TenantProspect::whereIn('registered_by', $partnerIds)
->pluck('id')
->toArray();
// 인계 완료된 가망고객의 management_id 조회
$handoverManagements = SalesTenantManagement::whereIn('tenant_prospect_id', $handoverProspectIds)
->where('hq_status', SalesTenantManagement::HQ_STATUS_HANDOVER)
->get();
// 인계 완료된 계약의 가입비 합계
$handoverManagementIds = $handoverManagements->pluck('id')->toArray();
$handoverTotalRegFee = SalesContractProduct::whereIn('management_id', $handoverManagementIds)
->sum('registration_fee');
// 수당 계산: 가입비 × 50% × 20% = 가입비 × 10%
$handoverPartnerCommission = (int)($handoverTotalRegFee * 0.10);
// 내가 매니저로 지정된 인계 완료 건의 수당 계산
$managedHandoverManagements = SalesTenantManagement::where('manager_user_id', $currentUserId)
->where('hq_status', SalesTenantManagement::HQ_STATUS_HANDOVER)
->get();
$managedHandoverManagementIds = $managedHandoverManagements->pluck('id')->toArray();
$managedHandoverTotalRegFee = SalesContractProduct::whereIn('management_id', $managedHandoverManagementIds)
->sum('registration_fee');
// 매니저 수당: 가입비 × 50% × 5% = 가입비 × 2.5%
$handoverManagerCommission = (int)($managedHandoverTotalRegFee * 0.025);
// 기존 수당에 인계 완료 수당 추가
$partnerCommissionTotal += $handoverPartnerCommission;
$managerCommissionTotal += $handoverManagerCommission;
$totalMembershipFee += $handoverTotalRegFee;
$totalCommission = $partnerCommissionTotal + $managerCommissionTotal;
// 역할별 수당 업데이트
$commissionByRole[0]['amount'] = $partnerCommissionTotal;
$commissionByRole[1]['amount'] = $managerCommissionTotal;
// 총 가입비 대비 수당 비율
$totalCommissionRatio = $totalMembershipFee > 0 ? round(($totalCommission / $totalMembershipFee) * 100, 1) : 0;
// 1) 내가 등록한 가망고객에서 전환된 tenant_id (20% 수당)
$registeredTenantIds = TenantProspect::whereNotNull('tenant_id')
->where('status', TenantProspect::STATUS_CONVERTED)
->whereIn('registered_by', $partnerIds)
->pluck('tenant_id')
->toArray();
// 2) 내가 매니저로 지정된 tenant_id (5% 수당)
$managedTenantIds = SalesTenantManagement::where('manager_user_id', $currentUserId)
->whereNotNull('tenant_id')
->pluck('tenant_id')
->toArray();
// 두 목록 합치기 (중복 제거)
$convertedTenantIds = array_unique(array_merge($registeredTenantIds, $managedTenantIds));
// 3) 인계 완료된 가망고객 ID 조회
$handoverCompletedProspectIds = SalesTenantManagement::whereNotNull('tenant_prospect_id')
->where('hq_status', SalesTenantManagement::HQ_STATUS_HANDOVER)
->pluck('tenant_prospect_id')
->toArray();
// 4) 내가 직접 등록한 가망고객 (진행중 - 인계 완료되지 않은 것)
// 하위 파트너가 등록한 것은 "유치 파트너 현황" 탭에서 표시
$prospects = TenantProspect::where('registered_by', $currentUserId)
->whereIn('status', [TenantProspect::STATUS_ACTIVE, TenantProspect::STATUS_EXPIRED])
->whereNotIn('id', $handoverCompletedProspectIds)
->orderBy('created_at', 'desc')
->get();
// 5) 내가 직접 등록하고 인계 완료된 가망고객 (히스토리)
$handoverProspects = TenantProspect::where('registered_by', $currentUserId)
->whereIn('id', $handoverCompletedProspectIds)
->orderBy('created_at', 'desc')
->get();
// 인계 완료된 가망고객 수
$handoverProspectCount = count($handoverManagementIds);
// 수익 및 테넌트 관리 통계 (실제 데이터)
$tenantStats = [
'total_tenants' => count($convertedTenantIds) + $handoverProspectCount, // 관리 테넌트 + 인계완료
'total_prospects' => $prospects->count(), // 진행중 가망고객
'total_membership_revenue' => $totalMembershipFee, // 총 가입비 실적
'total_commission_accumulated' => $totalCommission, // 누적 수당
'confirmed_commission' => $paidCommission, // 확정(지급완료) 수당
];
// 통계 업데이트
$stats['total_membership_fee'] = $totalMembershipFee;
$stats['total_commission'] = $totalCommission;
// 전환된 테넌트만 조회 (최신순, 페이지네이션)
$tenants = Tenant::whereIn('id', $convertedTenantIds)
->orderBy('created_at', 'desc')
->paginate(10)
->withQueryString();
// 각 테넌트의 영업 관리 정보 로드
$tenantIds = $tenants->pluck('id')->toArray();
$managements = SalesTenantManagement::whereIn('tenant_id', $tenantIds)
->with('manager')
->get()
->keyBy('tenant_id');
// 내가 유치한 영업파트너 목록 (드롭다운용)
$allManagers = auth()->user()->children()
->where('is_active', true)
->get(['id', 'name', 'email']);
return compact(
'stats',
'commissionByRole',
'totalCommissionRatio',
'tenantStats',
'tenants',
'prospects',
'handoverProspects',
'managements',
'allManagers',
'period',
'year',
'month',
'startDate',
'endDate'
);
}
/**
* 매니저 지정 변경
*/
public function assignManager(int $tenantId, Request $request): JsonResponse
{
$request->validate([
'manager_id' => 'required|integer',
]);
$tenant = Tenant::findOrFail($tenantId);
$managerId = $request->input('manager_id');
// 테넌트 영업 관리 정보 조회 또는 생성
$management = SalesTenantManagement::findOrCreateByTenant($tenantId);
if ($managerId === 0) {
// 본인으로 설정 (현재 로그인 사용자)
$manager = auth()->user();
$management->update([
'manager_user_id' => $manager->id,
]);
} else {
// 특정 매니저 지정
$manager = User::find($managerId);
if (!$manager) {
return response()->json([
'success' => false,
'message' => '매니저를 찾을 수 없습니다.',
], 404);
}
$management->update([
'manager_user_id' => $manager->id,
]);
}
return response()->json([
'success' => true,
'manager' => [
'id' => $manager->id,
'name' => $manager->name,
],
]);
}
/**
* 테넌트 리스트 부분 새로고침 (HTMX)
*/
public function refreshTenantList(Request $request): View
{
// 테넌트 목록 (나와 연결된 계약만)
$currentUserId = auth()->id();
$childrenIds = auth()->user()->children()->pluck('id')->toArray();
$partnerIds = array_merge([$currentUserId], $childrenIds);
// 1) 내가 등록한 가망고객에서 전환된 tenant_id (20% 수당)
$registeredTenantIds = TenantProspect::whereNotNull('tenant_id')
->where('status', TenantProspect::STATUS_CONVERTED)
->whereIn('registered_by', $partnerIds)
->pluck('tenant_id')
->toArray();
// 2) 내가 매니저로 지정된 tenant_id (5% 수당)
$managedTenantIds = SalesTenantManagement::where('manager_user_id', $currentUserId)
->pluck('tenant_id')
->toArray();
// 두 목록 합치기 (중복 제거)
$convertedTenantIds = array_unique(array_merge($registeredTenantIds, $managedTenantIds));
// 3) 내가 직접 등록한 가망고객 (아직 전환되지 않은 것 - active 상태)
$prospects = TenantProspect::where('registered_by', $currentUserId)
->whereIn('status', [TenantProspect::STATUS_ACTIVE, TenantProspect::STATUS_EXPIRED])
->orderBy('created_at', 'desc')
->get();
// 전환된 테넌트만 조회 (최신순, 페이지네이션)
$tenants = Tenant::whereIn('id', $convertedTenantIds)
->orderBy('created_at', 'desc')
->paginate(10)
->withQueryString();
// 각 테넌트의 영업 관리 정보 로드
$tenantIds = $tenants->pluck('id')->toArray();
$managements = SalesTenantManagement::whereIn('tenant_id', $tenantIds)
->with('manager')
->get()
->keyBy('tenant_id');
// 내가 유치한 영업파트너 목록 (드롭다운용)
$allManagers = auth()->user()->children()
->where('is_active', true)
->get(['id', 'name', 'email']);
return view('sales.dashboard.partials.tenant-list', compact(
'tenants',
'prospects',
'managements',
'allManagers'
));
}
/**
* 매니저 목록 조회 (드롭다운용)
*/
public function getManagers(Request $request): JsonResponse
{
// HQ 테넌트의 사용자 중 매니저 역할이 있는 사용자 조회
$managers = User::whereHas('tenants', function ($query) {
$query->where('tenant_type', 'HQ');
})->get(['id', 'name', 'email']);
return response()->json([
'success' => true,
'managers' => $managers,
]);
}
/**
* 유치 파트너 활동 현황 (HTMX 탭 로드)
*/
public function partnerActivity(Request $request): View
{
$data = $this->getPartnerActivityData();
return view('sales.dashboard.partials.partner-activity', $data);
}
/**
* 유치 파트너 활동 데이터 조회
*/
private function getPartnerActivityData(): array
{
$currentUser = auth()->user();
$currentUserId = $currentUser->id;
// 직접 유치한 하위 파트너 목록 (parent_id가 현재 사용자인 사용자들)
$recruitedPartners = User::where('parent_id', $currentUserId)
->where('is_active', true)
->with(['userRoles.role'])
->get();
$partnerIds = $recruitedPartners->pluck('id')->toArray();
// 요약 통계 계산
$summaryStats = $this->calculatePartnerSummaryStats($partnerIds, $currentUserId);
// 파트너별 상세 활동 데이터
$partnerActivities = $this->getPartnerActivitiesDetail($recruitedPartners, $currentUserId);
return [
'summaryStats' => $summaryStats,
'partnerActivities' => $partnerActivities,
'recruitedPartners' => $recruitedPartners,
];
}
/**
* 유치 파트너 요약 통계 계산
*/
private function calculatePartnerSummaryStats(array $partnerIds, int $currentUserId): array
{
// 유치 파트너 수
$partnerCount = count($partnerIds);
// 하위 파트너들이 등록한 총 영업권(명함) 수
$totalProspects = TenantProspect::whereIn('registered_by', $partnerIds)->count();
// 하위 파트너들의 계약 성사 건수
$totalConversions = TenantProspect::whereIn('registered_by', $partnerIds)
->where('status', TenantProspect::STATUS_CONVERTED)
->count();
// 확정 수당 (SalesCommission에서)
$confirmedCommission = SalesCommission::where('manager_user_id', $currentUserId)
->whereHas('partner', function ($query) use ($partnerIds) {
$query->whereIn('user_id', $partnerIds);
})
->sum('manager_commission');
// 예상 수당: 하위 파트너들이 등록한 가망고객의 가입비 × 5%
$prospectIds = TenantProspect::whereIn('registered_by', $partnerIds)->pluck('id')->toArray();
$managementIds = SalesTenantManagement::whereIn('tenant_prospect_id', $prospectIds)->pluck('id')->toArray();
$totalRegistrationFee = SalesContractProduct::whereIn('management_id', $managementIds)->sum('registration_fee');
$expectedFromFee = (int)($totalRegistrationFee * 0.05);
// 최종 예상 수당 (확정 + 예상 중 큰 값)
$expectedCommission = max($confirmedCommission, $expectedFromFee);
return [
'partner_count' => $partnerCount,
'total_prospects' => $totalProspects,
'total_conversions' => $totalConversions,
'expected_commission' => $expectedCommission,
];
}
/**
* 파트너별 상세 활동 데이터
*/
private function getPartnerActivitiesDetail($recruitedPartners, int $currentUserId): array
{
$activities = [];
foreach ($recruitedPartners as $partner) {
// 파트너의 영업파트너 정보
$salesPartner = SalesPartner::where('user_id', $partner->id)->first();
// 파트너가 등록한 영업권 수
$prospectCount = TenantProspect::where('registered_by', $partner->id)->count();
// 진행 중인 영업권 (active 상태)
$activeProspects = TenantProspect::where('registered_by', $partner->id)
->where('status', TenantProspect::STATUS_ACTIVE)
->count();
// 계약 성사 건수
$conversions = TenantProspect::where('registered_by', $partner->id)
->where('status', TenantProspect::STATUS_CONVERTED)
->count();
// 이 파트너로 인한 나의 매니저 수당 (확정 수당)
$confirmedCommission = 0;
if ($salesPartner) {
$confirmedCommission = SalesCommission::where('manager_user_id', $currentUserId)
->where('partner_id', $salesPartner->id)
->sum('manager_commission');
}
// 예상 수당 계산: 파트너가 등록한 가망고객의 가입비 × 5%
$prospectIds = TenantProspect::where('registered_by', $partner->id)->pluck('id')->toArray();
$managementIds = SalesTenantManagement::whereIn('tenant_prospect_id', $prospectIds)->pluck('id')->toArray();
$totalRegistrationFee = SalesContractProduct::whereIn('management_id', $managementIds)->sum('registration_fee');
$expectedCommission = (int)($totalRegistrationFee * 0.05); // 5% 매니저 수당
// 최종 매니저 수당 (확정 + 예상 중 큰 값, 또는 합산)
$managerCommission = max($confirmedCommission, $expectedCommission);
$hasRegistrationFee = $totalRegistrationFee > 0;
// 최근 활동 내역 (최근 전환된 테넌트 5개)
$recentTenants = TenantProspect::where('registered_by', $partner->id)
->where('status', TenantProspect::STATUS_CONVERTED)
->with(['tenant'])
->orderBy('converted_at', 'desc')
->limit(5)
->get();
// 파트너의 모든 가망고객 (진행률 조회용)
$allProspects = TenantProspect::where('registered_by', $partner->id)
->whereIn('status', [TenantProspect::STATUS_ACTIVE, TenantProspect::STATUS_EXPIRED])
->orderBy('created_at', 'desc')
->get();
// 활동 상태 판단
$lastActivity = TenantProspect::where('registered_by', $partner->id)
->orderBy('updated_at', 'desc')
->first();
$status = 'inactive';
if ($lastActivity) {
$daysSinceActivity = now()->diffInDays($lastActivity->updated_at);
if ($daysSinceActivity <= 7) {
$status = 'active';
} elseif ($daysSinceActivity <= 30) {
$status = 'moderate';
}
}
// 역할 정보
$roles = $partner->userRoles->pluck('role.name')->filter()->toArray();
$roleLabel = !empty($roles) ? implode(', ', $roles) : '영업';
$activities[] = [
'partner' => $partner,
'role_label' => $roleLabel,
'prospect_count' => $prospectCount,
'active_prospects' => $activeProspects,
'conversions' => $conversions,
'manager_commission' => $managerCommission,
'has_registration_fee' => $hasRegistrationFee,
'status' => $status,
'recent_tenants' => $recentTenants,
'all_prospects' => $allProspects,
];
}
return $activities;
}
/**
* 영업파트너 수당 정보 조회
*/
private function getCommissionData(): array
{
$user = auth()->user();
$commissionSummary = [];
$recentCommissions = collect();
// 현재 사용자가 영업파트너인지 확인
$partner = SalesPartner::where('user_id', $user->id)
->where('status', 'active')
->first();
if ($partner) {
$commissionSummary = $this->commissionService->getPartnerCommissionSummary($partner->id);
$recentCommissions = $this->commissionService->getRecentCommissions($partner->id, 5);
}
return compact('commissionSummary', 'recentCommissions', 'partner');
}
/**
* 영업파트너 가이드북 도움말 모달
*/
public function helpGuide(): View
{
// 가이드북 마크다운 파일 읽기 (resources/markdown 폴더)
$guidePath = resource_path('markdown/영업파트너가이드북.md');
if (file_exists($guidePath)) {
$markdown = file_get_contents($guidePath);
$htmlContent = Str::markdown($markdown);
} else {
$htmlContent = '<p class="text-gray-500">가이드북을 찾을 수 없습니다.</p>';
}
return view('sales.dashboard.partials.help-modal', compact('htmlContent'));
}
}