feat:가망고객 상태 토글 기능 추가 (영업중 ↔ 완료)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
김보곤
2026-02-07 08:41:04 +09:00
parent b34924ccab
commit 0ac8b3ee9b
5 changed files with 95 additions and 1 deletions

View File

@@ -209,6 +209,7 @@ private function getIndexData(Request $request): array
'active' => TenantProspect::where('status', TenantProspect::STATUS_ACTIVE)->count(),
'expired' => TenantProspect::where('status', TenantProspect::STATUS_EXPIRED)->count(),
'converted' => TenantProspect::where('status', TenantProspect::STATUS_CONVERTED)->count(),
'completed' => TenantProspect::where('status', TenantProspect::STATUS_COMPLETED)->count(),
'progress_complete' => $progressCompleteCount,
];
@@ -334,6 +335,34 @@ public function updateCommissionDate(int $id, Request $request)
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,
]);
}
/**
* 가망고객 삭제 (슈퍼관리자 전용)
*/

View File

@@ -21,6 +21,7 @@ class TenantProspect extends Model
public const STATUS_ACTIVE = 'active'; // 영업권 유효
public const STATUS_EXPIRED = 'expired'; // 영업권 만료
public const STATUS_CONVERTED = 'converted'; // 테넌트 전환 완료
public const STATUS_COMPLETED = 'completed'; // 영업 완료
public const VALIDITY_MONTHS = 2; // 영업권 유효기간 (개월)
public const COOLDOWN_MONTHS = 1; // 재등록 대기 기간 (개월)
@@ -101,6 +102,14 @@ public function isConverted(): bool
return $this->status === self::STATUS_CONVERTED;
}
/**
* 영업 완료 여부
*/
public function isCompleted(): bool
{
return $this->status === self::STATUS_COMPLETED;
}
/**
* 재등록 대기 중 여부
*/
@@ -126,6 +135,10 @@ public function getStatusLabelAttribute(): string
return '계약완료';
}
if ($this->isCompleted()) {
return '완료';
}
if ($this->isActive()) {
return '영업중';
}
@@ -146,6 +159,10 @@ public function getStatusColorAttribute(): string
return 'bg-green-100 text-green-800';
}
if ($this->isCompleted()) {
return 'bg-emerald-100 text-emerald-800';
}
if ($this->isActive()) {
return 'bg-blue-100 text-blue-800';
}

View File

@@ -101,6 +101,36 @@ class="refresh-btn inline-flex items-center gap-1.5 px-4 py-2 text-sm text-gray-
@push('scripts')
<script>
function toggleProspectStatus(prospectId) {
const btn = document.getElementById(`status-btn-${prospectId}`);
const currentLabel = btn.textContent.trim();
const newLabel = currentLabel === '영업중' ? '완료' : '영업중';
if (!confirm(`상태를 "${currentLabel}"에서 "${newLabel}"(으)로 변경하시겠습니까?`)) return;
fetch(`/sales/admin-prospects/${prospectId}/toggle-status`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(result => {
if (result.success) {
btn.textContent = result.status_label;
btn.className = `px-2 py-1 text-xs font-medium rounded-full cursor-pointer hover:opacity-80 transition ${result.status_color}`;
} else {
alert(result.message || '상태 변경에 실패했습니다.');
}
})
.catch(error => {
console.error('Error:', error);
alert('상태 변경 중 오류가 발생했습니다.');
});
}
function updateHqStatus(prospectId, status) {
const selectEl = event.target;
const originalValue = selectEl.dataset.originalValue || selectEl.value;

View File

@@ -1,5 +1,5 @@
{{-- 통계 카드 --}}
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-4">
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="text-sm text-gray-500">전체 고객</div>
<div class="text-2xl font-bold text-gray-800">{{ number_format($stats['total']) }}</div>
@@ -12,6 +12,10 @@
<div class="text-sm text-orange-600">영업권 만료</div>
<div class="text-2xl font-bold text-orange-800">{{ number_format($stats['expired']) }}</div>
</div>
<div class="bg-teal-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-teal-600">완료</div>
<div class="text-2xl font-bold text-teal-800">{{ number_format($stats['completed']) }}</div>
</div>
<div class="bg-emerald-50 rounded-lg shadow-sm p-4">
<div class="text-sm text-emerald-600">계약 완료</div>
<div class="text-2xl font-bold text-emerald-800">{{ number_format($stats['converted']) }}</div>
@@ -35,6 +39,10 @@ class="px-3 py-2 rounded-lg text-sm font-medium transition {{ request('status')
class="px-3 py-2 rounded-lg text-sm font-medium transition {{ request('status') === 'progress_complete' ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
진행완료
</a>
<a href="{{ route('sales.admin-prospects.index', array_merge(request()->except('page'), ['status' => 'completed'])) }}"
class="px-3 py-2 rounded-lg text-sm font-medium transition {{ request('status') === 'completed' ? 'bg-teal-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
완료
</a>
<a href="{{ route('sales.admin-prospects.index', array_merge(request()->except('page'), ['status' => 'converted'])) }}"
class="px-3 py-2 rounded-lg text-sm font-medium transition {{ request('status') === 'converted' ? 'bg-emerald-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
계약완료
@@ -206,9 +214,18 @@ class="text-xs font-medium rounded-lg px-2 py-1 border cursor-pointer
</select>
</td>
<td class="px-4 py-3 whitespace-nowrap">
@if(in_array($prospect->status, ['active', 'completed']))
<button type="button"
id="status-btn-{{ $prospect->id }}"
onclick="toggleProspectStatus({{ $prospect->id }})"
class="px-2 py-1 text-xs font-medium rounded-full cursor-pointer hover:opacity-80 transition {{ $prospect->status_color }}">
{{ $prospect->status_label }}
</button>
@else
<span class="px-2 py-1 text-xs font-medium rounded-full {{ $prospect->status_color }}">
{{ $prospect->status_label }}
</span>
@endif
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
{{ $prospect->created_at->format('Y-m-d') }}

View File

@@ -1152,6 +1152,7 @@
Route::post('admin-prospects/{id}/commission-date', [\App\Http\Controllers\Sales\AdminProspectController::class, 'updateCommissionDate'])->name('admin-prospects.update-commission-date');
Route::delete('admin-prospects/{id}/commission-date', [\App\Http\Controllers\Sales\AdminProspectController::class, 'clearCommissionDate'])->name('admin-prospects.clear-commission-date');
Route::delete('admin-prospects/{id}', [\App\Http\Controllers\Sales\AdminProspectController::class, 'destroy'])->name('admin-prospects.destroy')->middleware('super.admin');
Route::post('admin-prospects/{id}/toggle-status', [\App\Http\Controllers\Sales\AdminProspectController::class, 'toggleStatus'])->name('admin-prospects.toggle-status');
// 영업 시나리오 관리
Route::prefix('scenarios')->name('scenarios.')->group(function () {