Files
sam-manage/app/Http/Controllers/Sales/SalesDashboardController.php
김보곤 12c6175470 refactor:용어 변경 - 가입비 → 개발비
영업 관련 코드 및 문서 전체에서 "가입비"를 "개발비"로 변경
- 컨트롤러, 서비스, 모델
- 뷰 템플릿 (blade 파일)
- 가이드북 문서 (마크다운)
- 설정 파일

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

1006 lines
41 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\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;
// 역할별 수당 업데이트
$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;
// 1차/2차 수당 지급 현황 계산 (예상 수당 기반)
$commissionSummary = $this->calculateExpectedCommissionSummary(
$myCommissionsAsPartner,
$totalExpectedCommission,
$paidCommission
);
// 전환된 테넌트만 조회 (최신순, 페이지네이션)
$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);
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');
}
// 예상 수당 계산: 파트너가 등록한 가망고객의 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'));
}
/**
* 예상 수당 요약 계산 (개발 진행중 + 인계완료 미지급)
*/
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, // 지급완료
];
}
}