Files
sam-manage/app/Http/Controllers/Sales/SalesDashboardController.php

1073 lines
44 KiB
PHP
Raw Normal View History

<?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\SalesScenarioChecklist;
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);
// 영업파트너 수당 정보 추가 (recentCommissions, partner만 - commissionSummary는 getDashboardData에서 이미 계산됨)
$commissionData = $this->getCommissionData();
$data['recentCommissions'] = $commissionData['recentCommissions'];
$data['partner'] = $commissionData['partner'];
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' => null, // 1개월 구독료 (퍼센트가 아닌 고정 금액)
'rate_label' => '1개월 구독료',
'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();
// 매니저 수당: 1개월 구독료 (퍼센트가 아닌 고정 금액)
$handoverManagerCommission = (int)SalesContractProduct::whereIn('management_id', $managedHandoverManagementIds)
->sum('subscription_fee');
// 기존 수당에 인계 완료 수당 추가
$partnerCommissionTotal += $handoverPartnerCommission;
$managerCommissionTotal += $handoverManagerCommission;
$totalMembershipFee += $handoverTotalRegFee;
$totalCommission = $partnerCommissionTotal + $managerCommissionTotal;
// 역할별 수당 업데이트 (실제 지급된 수당 기준)
// 참고: 예상 수당은 나중에 $totalExpectedCommission으로 별도 계산됨
$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;
// === 예상 수당 계산 (개발 진행 중 + 인계 완료 중 지급 미완료) ===
// 1) 내가 등록한 가망고객 중 개발 진행 중인 건 (hq_status가 review 이상, handover 미만)
$myProspectIds = TenantProspect::whereIn('registered_by', $partnerIds)->pluck('id')->toArray();
$devInProgressStatuses = [
SalesTenantManagement::HQ_STATUS_REVIEW,
SalesTenantManagement::HQ_STATUS_PLANNING,
SalesTenantManagement::HQ_STATUS_CODING,
SalesTenantManagement::HQ_STATUS_DEV_TEST,
SalesTenantManagement::HQ_STATUS_DEV_DONE,
SalesTenantManagement::HQ_STATUS_INT_TEST,
];
$devInProgressManagementIds = SalesTenantManagement::whereIn('tenant_prospect_id', $myProspectIds)
->whereIn('hq_status', $devInProgressStatuses)
->pluck('id')
->toArray();
$devInProgressRegFee = SalesContractProduct::whereIn('management_id', $devInProgressManagementIds)
->sum('registration_fee');
$expectedFromDevInProgress = (int)($devInProgressRegFee * 0.10); // 개발비 × 10%
// 2) 인계 완료 중 지급 미완료 건
$handoverUnpaidRegFee = SalesContractProduct::whereIn('management_id', $handoverManagementIds)
->sum('registration_fee');
// 지급 완료된 금액 차감
$paidCommissionFromHandover = $myCommissionsAsPartner
->whereIn('management_id', $handoverManagementIds)
->where('status', SalesCommission::STATUS_PAID)
->sum('partner_commission');
$expectedFromHandover = (int)($handoverUnpaidRegFee * 0.10) - $paidCommissionFromHandover;
$expectedFromHandover = max(0, $expectedFromHandover);
// 총 예상 수당 (지급 완료 제외)
$totalExpectedCommission = $expectedFromDevInProgress + $expectedFromHandover;
// 지급대상 개발비 총액 (개발 진행 중 + 인계 완료 중 지급 미완료)
// 지급 완료된 건의 개발비는 제외
$paidManagementIds = $myCommissionsAsPartner
->where('status', SalesCommission::STATUS_PAID)
->pluck('management_id')
->toArray();
$unpaidHandoverRegFee = SalesContractProduct::whereIn('management_id', $handoverManagementIds)
->whereNotIn('management_id', $paidManagementIds)
->sum('registration_fee');
$targetDevelopmentFee = $devInProgressRegFee + $unpaidHandoverRegFee;
// stats 업데이트
$stats['target_development_fee'] = $targetDevelopmentFee;
// 1차/2차 수당 지급 현황 계산 (예상 수당 기반)
$commissionSummary = $this->calculateExpectedCommissionSummary(
$myCommissionsAsPartner,
$totalExpectedCommission,
$paidCommission
);
// 역할별 수당을 예상 수당 기준으로 재설정 (1차+2차 수당과 일치하도록)
// 판매자 예상 수당 = 개발비 × 10% (개발 진행 중 + 인계 완료 미지급)
$commissionByRole[0]['amount'] = $totalExpectedCommission;
// 전환된 테넌트만 조회 (최신순, 페이지네이션)
$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 = $this->getAllManagerUsers();
// 내가 매니저로만 참여하는 건 (다른 사람이 등록, 내가 매니저)
$managerOnlyProspects = $this->getManagerOnlyProspects($currentUserId);
return compact(
'stats',
'commissionByRole',
'totalCommissionRatio',
'tenantStats',
'tenants',
'prospects',
'handoverProspects',
'managements',
'allManagers',
'managerOnlyProspects',
'commissionSummary',
'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,
],
]);
}
/**
* 가망고객에 매니저 지정
*/
public function assignProspectManager(int $prospectId, Request $request): JsonResponse
{
$request->validate([
'manager_id' => 'required|integer',
]);
$prospect = TenantProspect::findOrFail($prospectId);
$managerId = $request->input('manager_id');
// 가망고객 영업 관리 정보 조회 또는 생성
$management = SalesTenantManagement::findOrCreateByProspect($prospectId);
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 = $this->getAllManagerUsers();
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);
// 내가 매니저로만 참여하는 가망고객 (다른 사람이 등록, 내가 매니저로 지정)
$managerOnlyProspects = $this->getManagerOnlyProspects($currentUserId);
return [
'summaryStats' => $summaryStats,
'partnerActivities' => $partnerActivities,
'recruitedPartners' => $recruitedPartners,
'managerOnlyProspects' => $managerOnlyProspects,
];
}
/**
* 내가 매니저로만 참여하는 조회
* (다른 사람이 등록했지만 내가 매니저로 지정된 )
*/
private function getManagerOnlyProspects(int $currentUserId): array
{
$results = [];
// 1. prospect 기반 매니저 지정 (가망고객 단계)
$prospectManagements = SalesTenantManagement::where('manager_user_id', $currentUserId)
->whereNotNull('tenant_prospect_id')
->with(['tenantProspect.registeredBy'])
->get();
foreach ($prospectManagements as $management) {
$prospect = $management->tenantProspect;
// 내가 등록한 건은 제외 (순수하게 매니저로만 참여한 건만)
if (!$prospect || $prospect->registered_by === $currentUserId) {
continue;
}
// 진행률 계산
$progress = SalesScenarioChecklist::getProspectProgress($prospect->id);
// 계약 금액 정보
$contractTotals = SalesContractProduct::where('management_id', $management->id)
->selectRaw('SUM(registration_fee) as total_registration_fee, SUM(subscription_fee) as total_subscription_fee')
->first();
$results[] = [
'type' => 'prospect',
'prospect' => $prospect,
'management' => $management,
'registeredBy' => $prospect->registeredBy,
'progress' => $progress,
'company_name' => $prospect->company_name,
'business_number' => $prospect->business_number,
'total_registration_fee' => $contractTotals->total_registration_fee ?? 0,
'total_subscription_fee' => $contractTotals->total_subscription_fee ?? 0,
];
}
// 2. tenant 기반 매니저 지정 (이미 계약된 고객)
$tenantManagements = SalesTenantManagement::where('manager_user_id', $currentUserId)
->whereNull('tenant_prospect_id')
->whereNotNull('tenant_id')
->with(['tenant'])
->get();
foreach ($tenantManagements as $management) {
$tenant = $management->tenant;
if (!$tenant) {
continue;
}
// 계약 금액 정보
$contractTotals = SalesContractProduct::where('management_id', $management->id)
->selectRaw('SUM(registration_fee) as total_registration_fee, SUM(subscription_fee) as total_subscription_fee')
->first();
$results[] = [
'type' => 'tenant',
'tenant' => $tenant,
'management' => $management,
'registeredBy' => null,
'progress' => [
'sales' => ['percentage' => 100, 'completed' => 0, 'total' => 0],
'manager' => ['percentage' => 100, 'completed' => 0, 'total' => 0],
],
'company_name' => $tenant->company_name,
'business_number' => $tenant->business_number,
'total_registration_fee' => $contractTotals->total_registration_fee ?? 0,
'total_subscription_fee' => $contractTotals->total_subscription_fee ?? 0,
];
}
return $results;
}
/**
* 유치 파트너 요약 통계 계산
*/
private function calculatePartnerSummaryStats(array $partnerIds, int $currentUserId): array
{
// 유치 파트너 수
$partnerCount = count($partnerIds);
// 하위 파트너들이 등록한 총 영업권(명함) 수
$totalProspects = TenantProspect::whereIn('registered_by', $partnerIds)->count();
// 예상 수당 계산을 위해 먼저 개발비/구독료 정보 조회
$prospectIds = TenantProspect::whereIn('registered_by', $partnerIds)->pluck('id')->toArray();
$managementIds = SalesTenantManagement::whereIn('tenant_prospect_id', $prospectIds)->pluck('id')->toArray();
// 하위 파트너들의 계약 건수 (개발비가 설정된 건수)
$contractedManagementCount = SalesContractProduct::whereIn('management_id', $managementIds)
->where('registration_fee', '>', 0)
->distinct('management_id')
->count('management_id');
$totalConversions = $contractedManagementCount;
// 매니저 예상 수당: 1개월 구독료 (퍼센트가 아닌 고정 금액)
$totalSubscriptionFee = SalesContractProduct::whereIn('management_id', $managementIds)->sum('subscription_fee');
// 확정 수당 (SalesCommission에서)
$confirmedCommission = SalesCommission::where('manager_user_id', $currentUserId)
->whereHas('partner', function ($query) use ($partnerIds) {
$query->whereIn('user_id', $partnerIds);
})
->sum('manager_commission');
$expectedFromFee = (int)$totalSubscriptionFee; // 1개월 구독료
// 최종 예상 수당 (확정 + 예상 중 큰 값)
$expectedCommission = max($confirmedCommission, $expectedFromFee);
// 지급 완료된 매니저 수당
$paidManagerCommission = SalesCommission::where('manager_user_id', $currentUserId)
->whereHas('partner', function ($query) use ($partnerIds) {
$query->whereIn('user_id', $partnerIds);
})
->where('status', SalesCommission::STATUS_PAID)
->sum('manager_commission');
// 지급예정 (승인됨)
$scheduledManagerCommission = SalesCommission::where('manager_user_id', $currentUserId)
->whereHas('partner', function ($query) use ($partnerIds) {
$query->whereIn('user_id', $partnerIds);
})
->where('status', SalesCommission::STATUS_APPROVED)
->sum('manager_commission');
// 입금대기 = 총 예상 - 지급완료 - 지급예정
$pendingManagerCommission = max(0, $expectedCommission - $paidManagerCommission - $scheduledManagerCommission);
// 1차/2차 분할 (각 50%)
$halfExpected = $expectedCommission / 2;
$halfPending = $pendingManagerCommission / 2;
$halfScheduled = $scheduledManagerCommission / 2;
$halfPaid = $paidManagerCommission / 2;
return [
'partner_count' => $partnerCount,
'total_prospects' => $totalProspects,
'total_conversions' => $totalConversions,
'expected_commission' => $expectedCommission,
'paid_commission' => $paidManagerCommission,
'first_commission' => [
'total' => (int)$halfExpected,
'pending' => (int)$halfPending,
'scheduled' => (int)$halfScheduled,
'paid' => (int)$halfPaid,
],
'second_commission' => [
'total' => (int)$halfExpected,
'pending' => (int)$halfPending,
'scheduled' => (int)$halfScheduled,
'paid' => (int)$halfPaid,
],
];
}
/**
* 파트너별 상세 활동 데이터
*/
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');
}
// 예상 수당 계산: 파트너가 등록한 가망고객의 1개월 구독료
$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');
$totalSubscriptionFee = SalesContractProduct::whereIn('management_id', $managementIds)->sum('subscription_fee');
$expectedCommission = (int)$totalSubscriptionFee; // 1개월 구독료
// 최종 매니저 수당 (확정 + 예상 중 큰 값, 또는 합산)
$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();
// 파트너가 매니저로 참여하는 건 (다른 사람이 등록, 이 파트너가 매니저)
$managerProspects = collect();
// 1. prospect 기반 매니저 지정 (가망고객 단계)
$prospectManagements = SalesTenantManagement::where('manager_user_id', $partner->id)
->whereNotNull('tenant_prospect_id')
->with(['tenantProspect.registeredBy'])
->get();
foreach ($prospectManagements as $mgmt) {
$prospect = $mgmt->tenantProspect;
// 본인이 등록한 건은 제외 (이미 allProspects에 포함됨)
if ($prospect && $prospect->registered_by !== $partner->id) {
$managerProspects->push([
'type' => 'prospect',
'prospect' => $prospect,
'management' => $mgmt,
'registeredBy' => $prospect->registeredBy,
'company_name' => $prospect->company_name,
'business_number' => $prospect->business_number,
]);
}
}
// 2. tenant 기반 매니저 지정 (이미 계약된 테넌트)
$tenantManagements = SalesTenantManagement::where('manager_user_id', $partner->id)
->whereNull('tenant_prospect_id')
->whereNotNull('tenant_id')
->with(['tenant'])
->get();
foreach ($tenantManagements as $mgmt) {
$tenant = $mgmt->tenant;
if ($tenant) {
$managerProspects->push([
'type' => 'tenant',
'tenant' => $tenant,
'management' => $mgmt,
'registeredBy' => null, // 테넌트는 등록자 정보 없음
'company_name' => $tenant->company_name,
'business_number' => $tenant->business_number,
]);
}
}
// 활동 상태 판단
$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,
'manager_prospects' => $managerProspects, // 매니저로만 참여하는 건
];
}
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');
}
/**
* 상담매니저 역할을 가진 모든 사용자 조회
* (tenant_id 조건 없이 - 파트너 관리 페이지와 동일한 방식)
*/
private function getAllManagerUsers()
{
return User::whereHas('userRoles.role', fn($q) => $q->where('name', 'manager'))
->where('is_active', true)
->where('id', '!=', auth()->id()) // 본인 제외
->get(['id', 'name', 'email']);
}
/**
* 매니저 검색 API (AJAX)
* (tenant_id 조건 없이 - 모든 상담매니저 검색 가능)
*/
public function searchManagers(Request $request): JsonResponse
{
$query = $request->input('q', '');
$authId = auth()->id();
// 디버깅: SQL 쿼리 로깅
\DB::enableQueryLog();
$managers = User::whereHas('userRoles.role', fn($q) => $q->where('name', 'manager'))
->where('is_active', true)
->where('id', '!=', $authId)
->when($query, function ($q) use ($query) {
$q->where(function ($subQ) use ($query) {
$subQ->where('name', 'like', "%{$query}%")
->orWhere('email', 'like', "%{$query}%");
});
})
->limit(10)
->get(['id', 'name', 'email']);
$sqlLog = \DB::getQueryLog();
\Log::info('searchManagers 디버그', [
'query' => $query,
'auth_id' => $authId,
'result_count' => $managers->count(),
'results' => $managers->pluck('name')->toArray(),
'sql' => $sqlLog,
]);
return response()->json([
'success' => true,
'managers' => $managers,
]);
}
/**
* 영업파트너 가이드북 도움말 모달
*/
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'));
}
/**
* 가망고객 개별 반환 (HTMX 동적 업데이트용)
*/
public function getProspectRow(int $prospectId): View
{
$prospect = TenantProspect::findOrFail($prospectId);
return view('sales.dashboard.partials.prospect-row', compact('prospect'));
}
/**
* 예상 수당 요약 계산 (개발 진행중 + 인계완료 미지급)
*/
private function calculateExpectedCommissionSummary($commissions, int $totalExpectedCommission, int $paidCommission): array
{
$thisMonth = now()->format('Y-m');
$thisMonthStart = now()->startOfMonth()->format('Y-m-d');
$thisMonthEnd = now()->endOfMonth()->format('Y-m-d');
// 지급예정 금액 (입금 완료, 지급 대기)
$scheduledAmount = $commissions
->whereIn('status', [SalesCommission::STATUS_PENDING, SalesCommission::STATUS_APPROVED])
->sum('partner_commission');
// 입금대기 금액 = 총 예상 수당 - 지급예정 - 지급완료
$pendingAmount = max(0, $totalExpectedCommission - $scheduledAmount - $paidCommission);
// 1차/2차 분할 (각 50%)
$halfExpected = $totalExpectedCommission / 2;
$halfPending = $pendingAmount / 2;
$halfScheduled = $scheduledAmount / 2;
$halfPaid = $paidCommission / 2;
return [
'scheduled_this_month' => $commissions
->where('status', SalesCommission::STATUS_APPROVED)
->filter(fn($c) => $c->scheduled_payment_date?->format('Y-m') === $thisMonth)
->sum('partner_commission'),
'total_received' => $paidCommission,
'pending_amount' => $pendingAmount,
'contracts_this_month' => $commissions
->filter(fn($c) => $c->payment_date >= $thisMonthStart && $c->payment_date <= $thisMonthEnd)
->count(),
'first_commission' => [
'total' => (int)$halfExpected,
'pending' => (int)$halfPending,
'scheduled' => (int)$halfScheduled,
'paid' => (int)$halfPaid,
],
'second_commission' => [
'total' => (int)$halfExpected,
'pending' => (int)$halfPending,
'scheduled' => (int)$halfScheduled,
'paid' => (int)$halfPaid,
],
'total_commission' => $totalExpectedCommission,
];
}
/**
* 수당 컬렉션에서 1/2 수당 요약 계산 (기존 방식 - 백업)
*/
private function calculateCommissionSummaryFromCollection($commissions): array
{
$thisMonth = now()->format('Y-m');
$thisMonthStart = now()->startOfMonth()->format('Y-m-d');
$thisMonthEnd = now()->endOfMonth()->format('Y-m-d');
// 1차 수당 계산
$firstCommission = $this->calculateStageCommissionFromCollection($commissions, 'first');
// 2차 수당 계산
$secondCommission = $this->calculateStageCommissionFromCollection($commissions, 'second');
return [
'scheduled_this_month' => $commissions
->where('status', SalesCommission::STATUS_APPROVED)
->filter(fn($c) => $c->scheduled_payment_date?->format('Y-m') === $thisMonth)
->sum('partner_commission'),
'total_received' => $commissions
->where('status', SalesCommission::STATUS_PAID)
->sum('partner_commission'),
'pending_amount' => $commissions
->where('status', SalesCommission::STATUS_PENDING)
->sum('partner_commission'),
'contracts_this_month' => $commissions
->filter(fn($c) => $c->payment_date >= $thisMonthStart && $c->payment_date <= $thisMonthEnd)
->count(),
'first_commission' => $firstCommission,
'second_commission' => $secondCommission,
'total_commission' => $commissions->sum('partner_commission'),
];
}
/**
* 단계별 수당 계산 (1/2)
* 파트너 수당의 50% 1/2차로 분할
*/
private function calculateStageCommissionFromCollection($commissions, string $stage): array
{
$paymentAtField = $stage === 'first' ? 'first_payment_at' : 'second_payment_at';
$paidAtField = $stage === 'first' ? 'first_partner_paid_at' : 'second_partner_paid_at';
$total = 0;
$pending = 0; // 납입 대기 (입금 전)
$scheduled = 0; // 지급예정 (입금 완료, 수당 미지급)
$paid = 0; // 지급완료
foreach ($commissions as $commission) {
// 파트너 수당의 50%가 각 단계별 금액
$stageAmount = $commission->partner_commission / 2;
$total += $stageAmount;
$paymentAt = $commission->{$paymentAtField};
$paidAt = $commission->{$paidAtField};
if ($paidAt) {
// 지급완료
$paid += $stageAmount;
} elseif ($paymentAt) {
// 납입완료, 수당 지급예정
$scheduled += $stageAmount;
} else {
// 납입 대기
$pending += $stageAmount;
}
}
return [
'total' => $total,
'pending' => $pending, // 납입 대기
'scheduled' => $scheduled, // 지급예정
'paid' => $paid, // 지급완료
];
}
}