Files
sam-manage/app/Http/Controllers/Sales/AdminProspectController.php
2026-02-25 11:45:01 +09:00

661 lines
25 KiB
PHP

<?php
namespace App\Http\Controllers\Sales;
use App\Http\Controllers\Controller;
use App\Models\Sales\SalesCommission;
use App\Models\Sales\SalesPartner;
use App\Models\Sales\SalesScenarioChecklist;
use App\Models\Sales\SalesTenantManagement;
use App\Models\Sales\TenantProspect;
use App\Models\User;
use App\Services\SalesCommissionService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\View\View;
/**
* 관리자용 전체 영업파트너 고객 관리 컨트롤러
* 관리자/슈퍼관리자만 접근 가능
*/
class AdminProspectController extends Controller
{
/**
* 관리자 권한 체크
*/
private function checkAdminAccess(): void
{
if (! auth()->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);
$management->load(['contractProducts.product', 'contractProducts.category']);
// 수당 정보
$commission = $this->loadMergedCommission($management);
// 수당 정산 전체 레코드 (상세보기용)
$commissions = SalesCommission::where('management_id', $management->id)
->with(['partner.user', 'manager'])
->get();
// 파트너 타입
$partnerType = $management->salesPartner?->partner_type;
if (! $partnerType && $prospect->registered_by) {
$partnerType = SalesPartner::where('user_id', $prospect->registered_by)->value('partner_type');
}
$partnerType = $partnerType ?? 'individual';
return view('sales.admin-prospects.partials.show-modal', compact('prospect', 'management', 'progress', 'commission', 'commissions', 'partnerType'));
}
/**
* 콘텐츠 새로고침 (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->contracted_at = $management?->contracted_at;
$prospect->handover_at = $management?->handover_at;
$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;
$prospect->contracted_at = $management?->contracted_at;
$prospect->handover_at = $management?->handover_at;
// 수당 정보 (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);
$newStatus = $request->input('hq_status');
$updateData = ['hq_status' => $newStatus];
// 인계로 변경 시 오늘 날짜 자동 세팅, 다른 상태로 변경 시 초기화
if ($newStatus === SalesTenantManagement::HQ_STATUS_HANDOVER) {
$updateData['handover_at'] = now();
} else {
$updateData['handover_at'] = null;
}
$management->update($updateData);
return response()->json([
'success' => true,
'hq_status' => $management->hq_status,
'hq_status_label' => $management->hq_status_label,
'handover_at' => $management->handover_at?->format('Y-m-d'),
]);
}
/**
* 계약일 변경
*/
public function updateContractedDate(int $id, Request $request)
{
$this->checkAdminAccess();
$request->validate([
'contracted_at' => 'nullable|date',
]);
$prospect = TenantProspect::findOrFail($id);
$management = SalesTenantManagement::findOrCreateByProspect($prospect->id);
$management->update([
'contracted_at' => $request->input('contracted_at') ?: null,
]);
return response()->json([
'success' => true,
'contracted_at' => $management->contracted_at?->format('Y-m-d'),
]);
}
/**
* 인계일 수동 변경
*/
public function updateHandoverDate(int $id, Request $request)
{
$this->checkAdminAccess();
$request->validate([
'handover_at' => 'required|date',
]);
$prospect = TenantProspect::findOrFail($id);
$management = SalesTenantManagement::findOrCreateByProspect($prospect->id);
$management->update([
'handover_at' => $request->input('handover_at'),
]);
return response()->json([
'success' => true,
'handover_at' => $management->handover_at?->format('Y-m-d'),
]);
}
/**
* 수당 날짜 기록/수정
*/
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);
// 파트너 resolve → 요율 결정
$partner = $management->salesPartner;
if (! $partner && $prospect->registered_by) {
$partner = SalesPartner::where('user_id', $prospect->registered_by)->first();
}
$isGroup = $partner?->isGroup() ?? false;
$partnerRate = $partner?->commission_rate
?? ($isGroup ? SalesCommissionService::DEFAULT_GROUP_RATE : SalesCommissionService::DEFAULT_PARTNER_RATE);
$referrerRate = $isGroup
? SalesCommissionService::DEFAULT_GROUP_REFERRER_RATE
: SalesCommissionService::DEFAULT_INDIVIDUAL_REFERRER_RATE;
// 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' => $partnerRate,
'manager_rate' => 0,
'partner_commission' => 0,
'manager_commission' => 0,
'referrer_rate' => $referrerRate,
'referrer_commission' => 0,
'scheduled_payment_date' => now()->addMonth()->day(10),
'status' => SalesCommission::STATUS_PENDING,
'partner_id' => $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;
}
// 납입일 변경 시 지급예정일 + 입금일(payment_date)도 함께 업데이트
if (in_array($field, ['first_payment_at', 'second_payment_at'])) {
$updateData['scheduled_payment_date'] = \Carbon\Carbon::parse($date)->addMonth()->day(10)->format('Y-m-d');
$updateData['payment_date'] = $date;
}
$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 && $prospect->registered_by) {
$partner = SalesPartner::where('user_id', $prospect->registered_by)->first();
}
if ($partner && $partner->isGroup()) {
return response()->json([
'success' => false,
'message' => '단체 파트너는 협업지원금을 수동 변경할 수 없습니다.',
], 422);
}
$isGroup = $partner?->isGroup() ?? false;
$partnerRate = $partner?->commission_rate
?? ($isGroup ? SalesCommissionService::DEFAULT_GROUP_RATE : SalesCommissionService::DEFAULT_PARTNER_RATE);
// 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' => $partnerRate,
'manager_rate' => 0,
'partner_commission' => 0,
'manager_commission' => 0,
'referrer_rate' => $isGroup
? SalesCommissionService::DEFAULT_GROUP_REFERRER_RATE
: SalesCommissionService::DEFAULT_INDIVIDUAL_REFERRER_RATE,
'referrer_commission' => 0,
'scheduled_payment_date' => now()->addMonth()->day(10),
'status' => SalesCommission::STATUS_PENDING,
'partner_id' => $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,
]);
}
}