user()->isAdmin() && !auth()->user()->isSuperAdmin()) { abort(403, '관리자만 접근할 수 있습니다.'); } } /** * 필드명에 따라 payment_type 결정 (1차→deposit, 2차→balance) */ private function getPaymentTypeForField(string $field): string { return in_array($field, ['second_payment_at', 'second_partner_paid_at']) ? SalesCommission::PAYMENT_BALANCE : SalesCommission::PAYMENT_DEPOSIT; } /** * deposit/balance 커미션 레코드를 병합하여 뷰용 객체 반환 */ private function loadMergedCommission(?SalesTenantManagement $management): ?object { if (!$management) { return null; } $commissions = SalesCommission::where('management_id', $management->id)->get(); if ($commissions->isEmpty()) { return null; } $deposit = $commissions->firstWhere('payment_type', SalesCommission::PAYMENT_DEPOSIT); $balance = $commissions->firstWhere('payment_type', SalesCommission::PAYMENT_BALANCE); // balance 레코드가 없으면 기존 단일 레코드 그대로 반환 (하위호환) if (!$balance) { return $deposit ?? $commissions->first(); } // 1차 필드는 deposit, 2차 필드는 balance에서 가져옴 $merged = new \stdClass(); $merged->first_payment_at = $deposit?->first_payment_at; $merged->first_partner_paid_at = $deposit?->first_partner_paid_at; $merged->second_payment_at = $balance->second_payment_at; $merged->second_partner_paid_at = $balance->second_partner_paid_at; $merged->first_subscription_at = $deposit?->first_subscription_at; $merged->manager_paid_at = $deposit?->manager_paid_at; $merged->referrer_commission = ($deposit?->referrer_commission ?? 0) + ($balance?->referrer_commission ?? 0); return $merged; } /** * 전체 고객 목록 페이지 */ public function index(Request $request): View|Response { $this->checkAdminAccess(); if ($request->header('HX-Request')) { return response('', 200)->header('HX-Redirect', route('sales.admin-prospects.index')); } $data = $this->getIndexData($request); return view('sales.admin-prospects.index', $data); } /** * 고객 상세 모달 */ public function modalShow(int $id): View { $this->checkAdminAccess(); $prospect = TenantProspect::with(['registeredBy', 'tenant'])->findOrFail($id); // 진행률 $progress = SalesScenarioChecklist::getProspectProgress($prospect->id); $prospect->sales_progress = $progress['sales']['percentage']; $prospect->manager_progress = $progress['manager']['percentage']; // management 정보 $management = SalesTenantManagement::findOrCreateByProspect($prospect->id); return view('sales.admin-prospects.partials.show-modal', compact('prospect', 'management', 'progress')); } /** * 콘텐츠 새로고침 (HTMX) */ public function refresh(Request $request): View { $this->checkAdminAccess(); $data = $this->getIndexData($request); return view('sales.admin-prospects.partials.content', $data); } /** * index 데이터 조회 (공통) */ private function getIndexData(Request $request): array { // 영업 역할을 가진 사용자 목록 (영업파트너) $salesPartners = User::whereHas('userRoles', function ($q) { $q->whereHas('role', function ($rq) { $rq->whereIn('name', ['sales', 'manager']); }); })->orderBy('name')->get(); // 필터 $filters = [ 'search' => $request->get('search'), 'status' => $request->get('status'), 'registered_by' => $request->get('registered_by'), ]; // 쿼리 빌드 $query = TenantProspect::with(['registeredBy', 'tenant']); // 검색 if (!empty($filters['search'])) { $search = $filters['search']; $query->where(function ($q) use ($search) { $q->where('company_name', 'like', "%{$search}%") ->orWhere('business_number', 'like', "%{$search}%") ->orWhere('ceo_name', 'like', "%{$search}%") ->orWhere('contact_phone', 'like', "%{$search}%"); }); } // 상태 필터 $isProgressCompleteFilter = ($filters['status'] === 'progress_complete'); $isHandoverFilter = ($filters['status'] === 'handover'); if (!empty($filters['status']) && !$isProgressCompleteFilter && !$isHandoverFilter) { $query->where('status', $filters['status']); } // 인계완료 필터: hq_status가 handover인 prospect만 if ($isHandoverFilter) { $handoverProspectIds = SalesTenantManagement::where('hq_status', 'handover')->pluck('tenant_prospect_id'); $query->whereIn('id', $handoverProspectIds); } // 영업파트너 필터 if (!empty($filters['registered_by'])) { $query->where('registered_by', $filters['registered_by']); } // progress_complete 필터: 전체 조회 후 PHP에서 필터링 if ($isProgressCompleteFilter) { $allProspects = $query->orderByDesc('created_at')->get(); // 진행률 계산 및 부가정보 세팅 foreach ($allProspects as $prospect) { $progress = SalesScenarioChecklist::getProspectProgress($prospect->id); $prospect->sales_progress = $progress['sales']['percentage']; $prospect->manager_progress = $progress['manager']['percentage']; if ($progress['sales']['percentage'] === 100 && $progress['manager']['percentage'] === 100) { SalesScenarioChecklist::checkAndConvertProspectStatus($prospect->id); $prospect->refresh(); } $management = SalesTenantManagement::where('tenant_prospect_id', $prospect->id)->first(); $prospect->hq_status = $management?->hq_status ?? 'pending'; $prospect->hq_status_label = $management?->hq_status_label ?? '대기'; $prospect->manager_user = $management?->manager; $prospect->commission = $this->loadMergedCommission($management); // 파트너 타입: management → registered_by 순으로 조회 $partnerType = $management?->salesPartner?->partner_type; if (!$partnerType && $prospect->registered_by) { $partnerType = SalesPartner::where('user_id', $prospect->registered_by)->value('partner_type'); } $prospect->partner_type = $partnerType ?? 'individual'; } // 두 시나리오 모두 100%인 것만 필터링 $filtered = $allProspects->filter(function ($prospect) { return $prospect->sales_progress === 100 && $prospect->manager_progress === 100; }); // 수동 페이지네이션 $page = request()->get('page', 1); $perPage = 20; $prospects = new \Illuminate\Pagination\LengthAwarePaginator( $filtered->forPage($page, $perPage)->values(), $filtered->count(), $perPage, $page, ['path' => request()->url(), 'query' => request()->query()] ); } else { $prospects = $query->orderByDesc('created_at')->paginate(20); // 각 가망고객의 진행률 계산 및 상태 자동 전환 foreach ($prospects as $prospect) { $progress = SalesScenarioChecklist::getProspectProgress($prospect->id); $prospect->sales_progress = $progress['sales']['percentage']; $prospect->manager_progress = $progress['manager']['percentage']; // 진행률 100% 시 상태 자동 전환 체크 if ($progress['sales']['percentage'] === 100 && $progress['manager']['percentage'] === 100) { SalesScenarioChecklist::checkAndConvertProspectStatus($prospect->id); $prospect->refresh(); } // management 정보 $management = SalesTenantManagement::where('tenant_prospect_id', $prospect->id)->first(); $prospect->hq_status = $management?->hq_status ?? 'pending'; $prospect->hq_status_label = $management?->hq_status_label ?? '대기'; $prospect->manager_user = $management?->manager; // 수당 정보 (management가 있는 경우) $prospect->commission = $this->loadMergedCommission($management); // 파트너 타입: management → registered_by 순으로 조회 $partnerType = $management?->salesPartner?->partner_type; if (!$partnerType && $prospect->registered_by) { $partnerType = SalesPartner::where('user_id', $prospect->registered_by)->value('partner_type'); } $prospect->partner_type = $partnerType ?? 'individual'; } } // 진행완료 건수 계산 (전체 prospect 중 두 시나리오 모두 100%인 건수) $progressCompleteCount = 0; $allForStats = TenantProspect::all(); foreach ($allForStats as $p) { $prog = SalesScenarioChecklist::getProspectProgress($p->id); if ($prog['sales']['percentage'] === 100 && $prog['manager']['percentage'] === 100) { $progressCompleteCount++; } } // 전체 통계 $stats = [ 'total' => TenantProspect::count(), 'active' => TenantProspect::where('status', TenantProspect::STATUS_ACTIVE)->count(), 'completed' => TenantProspect::where('status', TenantProspect::STATUS_COMPLETED)->count(), 'handover' => SalesTenantManagement::where('hq_status', 'handover')->count(), ]; // 영업파트너별 통계 $partnerStats = TenantProspect::selectRaw('registered_by, COUNT(*) as total') ->groupBy('registered_by') ->with('registeredBy') ->get() ->map(function ($item) { return [ 'user' => $item->registeredBy, 'total' => $item->total, ]; }); $isSuperAdmin = auth()->user()->isSuperAdmin(); return compact('prospects', 'stats', 'salesPartners', 'partnerStats', 'filters', 'isSuperAdmin'); } /** * 개발 진행 상태 변경 */ public function updateHqStatus(int $id, Request $request) { $this->checkAdminAccess(); $request->validate([ 'hq_status' => 'required|in:' . implode(',', array_keys(SalesTenantManagement::$hqStatusLabels)), ]); $prospect = TenantProspect::findOrFail($id); $management = SalesTenantManagement::findOrCreateByProspect($prospect->id); $management->update([ 'hq_status' => $request->input('hq_status'), ]); return response()->json([ 'success' => true, 'hq_status' => $management->hq_status, 'hq_status_label' => $management->hq_status_label, ]); } /** * 수당 날짜 기록/수정 */ public function updateCommissionDate(int $id, Request $request) { $this->checkAdminAccess(); $request->validate([ 'field' => 'required|in:first_payment_at,first_partner_paid_at,second_payment_at,second_partner_paid_at,first_subscription_at,manager_paid_at', 'date' => 'nullable|date', ]); $prospect = TenantProspect::findOrFail($id); $management = SalesTenantManagement::findOrCreateByProspect($prospect->id); $field = $request->input('field'); // 1차 필드 → deposit 레코드, 2차 필드 → balance 레코드 $paymentType = $this->getPaymentTypeForField($field); // Commission 레코드 조회 또는 생성 (payment_type별 분리) $commission = SalesCommission::firstOrCreate( ['management_id' => $management->id, 'payment_type' => $paymentType], [ 'tenant_id' => $prospect->tenant_id ?? 1, 'payment_amount' => 0, 'payment_date' => now(), 'base_amount' => 0, 'partner_rate' => 0, 'manager_rate' => 0, 'partner_commission' => 0, 'manager_commission' => 0, 'referrer_rate' => 3, 'referrer_commission' => 0, 'scheduled_payment_date' => now()->addMonth()->day(10), 'status' => SalesCommission::STATUS_PENDING, 'partner_id' => $management->sales_partner_id ?? 0, 'manager_user_id' => $management->manager_user_id, ] ); $date = $request->input('date') ?: now()->format('Y-m-d'); // 수당지급일 필드는 개발상태가 '인계'일 때만 저장 가능 $paidFields = ['first_partner_paid_at', 'second_partner_paid_at', 'manager_paid_at']; if (in_array($field, $paidFields) && $management->hq_status !== 'handover') { return response()->json([ 'success' => false, 'message' => '개발상태가 인계일 때만 수당이 지급됩니다.', ], 422); } $updateData = [$field => $date]; // 납입일 입력 시 수당지급일 자동 계산 (익월 10일) - 인계 상태일 때만 $autoFields = [ 'first_payment_at' => 'first_partner_paid_at', 'second_payment_at' => 'second_partner_paid_at', ]; $autoField = null; $autoDate = null; if (isset($autoFields[$field]) && $management->hq_status === 'handover') { $autoField = $autoFields[$field]; $autoDate = \Carbon\Carbon::parse($date)->addMonth()->day(10)->format('Y-m-d'); $updateData[$autoField] = $autoDate; } $commission->update($updateData); $response = [ 'success' => true, 'field' => $field, 'date' => $commission->$field?->format('Y-m-d'), 'date_display' => $commission->$field?->format('m/d'), ]; if ($autoField) { $response['auto_field'] = $autoField; $response['auto_date'] = $autoDate; } return response()->json($response); } /** * 상태 토글 (영업중 ↔ 완료) */ public function toggleStatus(int $id) { $this->checkAdminAccess(); $prospect = TenantProspect::findOrFail($id); if ($prospect->status === TenantProspect::STATUS_ACTIVE) { $prospect->update(['status' => TenantProspect::STATUS_COMPLETED]); } elseif ($prospect->status === TenantProspect::STATUS_COMPLETED) { $prospect->update(['status' => TenantProspect::STATUS_ACTIVE]); } else { return response()->json([ 'success' => false, 'message' => '영업중 또는 완료 상태만 변경할 수 있습니다.', ], 422); } return response()->json([ 'success' => true, 'status' => $prospect->status, 'status_label' => $prospect->status_label, 'status_color' => $prospect->status_color, ]); } /** * 가망고객 삭제 (슈퍼관리자 전용) */ public function destroy(int $id) { $prospect = TenantProspect::findOrFail($id); // 연관 데이터 삭제 $management = SalesTenantManagement::where('tenant_prospect_id', $prospect->id)->first(); if ($management) { SalesCommission::where('management_id', $management->id)->delete(); $management->delete(); } SalesScenarioChecklist::where('tenant_prospect_id', $prospect->id)->delete(); $prospect->delete(); return response()->json([ 'success' => true, 'message' => "'{$prospect->company_name}' 가망고객이 삭제되었습니다.", ]); } /** * 협업지원금(referrer_commission) 금액 수정 */ public function updateReferrerCommission(int $id, Request $request) { $this->checkAdminAccess(); $request->validate([ 'amount' => 'required|numeric|min:0', ]); $prospect = TenantProspect::findOrFail($id); $management = SalesTenantManagement::findOrCreateByProspect($prospect->id); // 단체 파트너는 수동 수정 불가 $partner = $management->salesPartner; if ($partner && $partner->isGroup()) { return response()->json([ 'success' => false, 'message' => '단체 파트너는 협업지원금을 수동 변경할 수 없습니다.', ], 422); } // Commission 레코드 조회 또는 생성 $commission = SalesCommission::firstOrCreate( ['management_id' => $management->id], [ 'tenant_id' => $prospect->tenant_id ?? 1, 'payment_type' => 'deposit', 'payment_amount' => 0, 'payment_date' => now(), 'base_amount' => 0, 'partner_rate' => 0, 'manager_rate' => 0, 'partner_commission' => 0, 'manager_commission' => 0, 'referrer_rate' => 3, 'referrer_commission' => 0, 'scheduled_payment_date' => now()->addMonth()->day(10), 'status' => SalesCommission::STATUS_PENDING, 'partner_id' => $management->sales_partner_id ?? 0, 'manager_user_id' => $management->manager_user_id, ] ); $commission->update([ 'referrer_commission' => $request->input('amount'), ]); return response()->json([ 'success' => true, 'amount' => (int) $commission->referrer_commission, ]); } /** * 수당 날짜 삭제 (초기화) */ public function clearCommissionDate(int $id, Request $request) { $this->checkAdminAccess(); $request->validate([ 'field' => 'required|in:first_payment_at,first_partner_paid_at,second_payment_at,second_partner_paid_at,first_subscription_at,manager_paid_at', ]); $prospect = TenantProspect::findOrFail($id); $management = SalesTenantManagement::where('tenant_prospect_id', $prospect->id)->first(); if (!$management) { return response()->json(['success' => false, 'message' => '관리 정보가 없습니다.']); } $field = $request->input('field'); // 1차 필드 → deposit 레코드, 2차 필드 → balance 레코드 $paymentType = $this->getPaymentTypeForField($field); $commission = SalesCommission::where('management_id', $management->id) ->where('payment_type', $paymentType) ->first(); if (!$commission) { return response()->json(['success' => false, 'message' => '수당 정보가 없습니다.']); } $updateData = [$field => null]; // 납입일 삭제 시 수당지급일도 함께 초기화 $autoFields = [ 'first_payment_at' => 'first_partner_paid_at', 'second_payment_at' => 'second_partner_paid_at', ]; $autoField = null; if (isset($autoFields[$field])) { $autoField = $autoFields[$field]; $updateData[$autoField] = null; } $commission->update($updateData); return response()->json([ 'success' => true, 'field' => $field, 'auto_field' => $autoField, ]); } }