diff --git a/app/Http/Controllers/Sales/SalesManagerController.php b/app/Http/Controllers/Sales/SalesManagerController.php new file mode 100644 index 00000000..5ee1c506 --- /dev/null +++ b/app/Http/Controllers/Sales/SalesManagerController.php @@ -0,0 +1,155 @@ +header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('sales.managers.index')); + } + + $query = SalesManager::query()->active(); + + // 검색 + if ($search = $request->get('search')) { + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('member_id', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%") + ->orWhere('phone', 'like', "%{$search}%"); + }); + } + + // 역할 필터 + if ($role = $request->get('role')) { + $query->where('role', $role); + } + + $managers = $query->orderBy('name')->paginate(20); + + // 통계 + $stats = [ + 'total' => SalesManager::active()->count(), + 'operators' => SalesManager::active()->where('role', 'operator')->count(), + 'sales_admins' => SalesManager::active()->where('role', 'sales_admin')->count(), + 'managers' => SalesManager::active()->where('role', 'manager')->count(), + ]; + + return view('sales.managers.index', compact('managers', 'stats')); + } + + /** + * 등록 폼 + */ + public function create(): View + { + $parents = SalesManager::active() + ->whereIn('role', ['operator', 'sales_admin']) + ->orderBy('name') + ->get(); + + return view('sales.managers.create', compact('parents')); + } + + /** + * 등록 처리 + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'member_id' => 'required|string|max:50|unique:sales_managers,member_id', + 'password' => 'required|string|min:4', + 'name' => 'required|string|max:100', + 'phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:100', + 'parent_id' => 'nullable|exists:sales_managers,id', + 'role' => 'required|in:operator,sales_admin,manager', + 'remarks' => 'nullable|string', + ]); + + SalesManager::create($validated); + + return redirect()->route('sales.managers.index') + ->with('success', '담당자가 등록되었습니다.'); + } + + /** + * 상세 페이지 + */ + public function show(int $id): View + { + $manager = SalesManager::with(['parent', 'children', 'registeredProspects', 'records']) + ->findOrFail($id); + + return view('sales.managers.show', compact('manager')); + } + + /** + * 수정 폼 + */ + public function edit(int $id): View + { + $manager = SalesManager::findOrFail($id); + $parents = SalesManager::active() + ->whereIn('role', ['operator', 'sales_admin']) + ->where('id', '!=', $id) + ->orderBy('name') + ->get(); + + return view('sales.managers.edit', compact('manager', 'parents')); + } + + /** + * 수정 처리 + */ + public function update(Request $request, int $id) + { + $manager = SalesManager::findOrFail($id); + + $validated = $request->validate([ + 'name' => 'required|string|max:100', + 'phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:100', + 'parent_id' => 'nullable|exists:sales_managers,id', + 'role' => 'required|in:operator,sales_admin,manager', + 'remarks' => 'nullable|string', + 'password' => 'nullable|string|min:4', + ]); + + // 비밀번호가 비어있으면 제외 + if (empty($validated['password'])) { + unset($validated['password']); + } + + $manager->update($validated); + + return redirect()->route('sales.managers.index') + ->with('success', '담당자 정보가 수정되었습니다.'); + } + + /** + * 삭제 처리 (비활성화) + */ + public function destroy(int $id) + { + $manager = SalesManager::findOrFail($id); + $manager->update(['is_active' => false]); + + return redirect()->route('sales.managers.index') + ->with('success', '담당자가 비활성화되었습니다.'); + } +} diff --git a/app/Http/Controllers/Sales/SalesProspectController.php b/app/Http/Controllers/Sales/SalesProspectController.php new file mode 100644 index 00000000..d602103c --- /dev/null +++ b/app/Http/Controllers/Sales/SalesProspectController.php @@ -0,0 +1,170 @@ +header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('sales.prospects.index')); + } + + $query = SalesProspect::with(['manager', 'salesManager', 'products']); + + // 검색 + if ($search = $request->get('search')) { + $query->where(function ($q) use ($search) { + $q->where('company_name', 'like', "%{$search}%") + ->orWhere('business_no', 'like', "%{$search}%") + ->orWhere('representative', 'like', "%{$search}%") + ->orWhere('contact_phone', 'like', "%{$search}%"); + }); + } + + // 상태 필터 + if ($status = $request->get('status')) { + $query->where('status', $status); + } + + // 담당자 필터 + if ($managerId = $request->get('manager_id')) { + $query->where(function ($q) use ($managerId) { + $q->where('manager_id', $managerId) + ->orWhere('sales_manager_id', $managerId); + }); + } + + $prospects = $query->orderByDesc('created_at')->paginate(20); + + // 통계 + $stats = [ + 'total' => SalesProspect::count(), + 'lead' => SalesProspect::where('status', 'lead')->count(), + 'prospect' => SalesProspect::where('status', 'prospect')->count(), + 'negotiation' => SalesProspect::where('status', 'negotiation')->count(), + 'contracted' => SalesProspect::where('status', 'contracted')->count(), + 'total_contract' => SalesProspectProduct::sum('contract_amount'), + 'total_commission' => SalesProspectProduct::sum('commission_amount'), + ]; + + $managers = SalesManager::active()->orderBy('name')->get(); + + return view('sales.prospects.index', compact('prospects', 'stats', 'managers')); + } + + /** + * 등록 폼 + */ + public function create(): View + { + $managers = SalesManager::active()->orderBy('name')->get(); + + return view('sales.prospects.create', compact('managers')); + } + + /** + * 등록 처리 + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'manager_id' => 'required|exists:sales_managers,id', + 'sales_manager_id' => 'nullable|exists:sales_managers,id', + 'company_name' => 'required|string|max:200', + 'representative' => 'nullable|string|max:100', + 'business_no' => 'nullable|string|max:20', + 'contact_phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:100', + 'address' => 'nullable|string|max:500', + 'status' => 'required|in:lead,prospect,negotiation,contracted,lost', + ]); + + $prospect = SalesProspect::create($validated); + + return redirect()->route('sales.prospects.show', $prospect->id) + ->with('success', '가망고객이 등록되었습니다.'); + } + + /** + * 상세 페이지 + */ + public function show(int $id): View + { + $prospect = SalesProspect::with([ + 'manager', + 'salesManager', + 'products', + 'scenarios', + 'consultations.manager' + ])->findOrFail($id); + + $managers = SalesManager::active() + ->whereIn('role', ['sales_admin', 'manager']) + ->orderBy('name') + ->get(); + + return view('sales.prospects.show', compact('prospect', 'managers')); + } + + /** + * 수정 폼 + */ + public function edit(int $id): View + { + $prospect = SalesProspect::findOrFail($id); + $managers = SalesManager::active()->orderBy('name')->get(); + + return view('sales.prospects.edit', compact('prospect', 'managers')); + } + + /** + * 수정 처리 + */ + public function update(Request $request, int $id) + { + $prospect = SalesProspect::findOrFail($id); + + $validated = $request->validate([ + 'sales_manager_id' => 'nullable|exists:sales_managers,id', + 'company_name' => 'required|string|max:200', + 'representative' => 'nullable|string|max:100', + 'business_no' => 'nullable|string|max:20', + 'contact_phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:100', + 'address' => 'nullable|string|max:500', + 'status' => 'required|in:lead,prospect,negotiation,contracted,lost', + ]); + + $prospect->update($validated); + + return redirect()->route('sales.prospects.show', $prospect->id) + ->with('success', '가망고객 정보가 수정되었습니다.'); + } + + /** + * 삭제 처리 + */ + public function destroy(int $id) + { + $prospect = SalesProspect::findOrFail($id); + $prospect->delete(); + + return redirect()->route('sales.prospects.index') + ->with('success', '가망고객이 삭제되었습니다.'); + } +} diff --git a/app/Http/Controllers/Sales/SalesRecordController.php b/app/Http/Controllers/Sales/SalesRecordController.php new file mode 100644 index 00000000..cc83d8e9 --- /dev/null +++ b/app/Http/Controllers/Sales/SalesRecordController.php @@ -0,0 +1,166 @@ +header('HX-Request')) { + return response('', 200)->header('HX-Redirect', route('sales.records.index')); + } + + $query = SalesRecord::with(['manager', 'prospect']); + + // 검색 + if ($search = $request->get('search')) { + $query->where(function ($q) use ($search) { + $q->where('description', 'like', "%{$search}%") + ->orWhere('record_type', 'like', "%{$search}%") + ->orWhereHas('prospect', function ($q2) use ($search) { + $q2->where('company_name', 'like', "%{$search}%"); + }); + }); + } + + // 상태 필터 + if ($status = $request->get('status')) { + $query->where('status', $status); + } + + // 담당자 필터 + if ($managerId = $request->get('manager_id')) { + $query->where('manager_id', $managerId); + } + + // 기간 필터 + if ($startDate = $request->get('start_date')) { + $query->where('record_date', '>=', $startDate); + } + if ($endDate = $request->get('end_date')) { + $query->where('record_date', '<=', $endDate); + } + + $records = $query->orderByDesc('record_date')->paginate(20); + + // 통계 + $stats = [ + 'total_count' => SalesRecord::count(), + 'pending_count' => SalesRecord::pending()->count(), + 'approved_count' => SalesRecord::approved()->count(), + 'total_amount' => SalesRecord::sum('amount'), + 'total_commission' => SalesRecord::sum('commission'), + 'pending_amount' => SalesRecord::pending()->sum('amount'), + 'approved_amount' => SalesRecord::approved()->sum('amount'), + ]; + + $managers = SalesManager::active()->orderBy('name')->get(); + + return view('sales.records.index', compact('records', 'stats', 'managers')); + } + + /** + * 등록 폼 + */ + public function create(): View + { + $managers = SalesManager::active()->orderBy('name')->get(); + $prospects = SalesProspect::orderBy('company_name')->get(); + + return view('sales.records.create', compact('managers', 'prospects')); + } + + /** + * 등록 처리 + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'manager_id' => 'required|exists:sales_managers,id', + 'prospect_id' => 'nullable|exists:sales_prospects,id', + 'record_date' => 'required|date', + 'record_type' => 'required|string|max:50', + 'amount' => 'required|numeric|min:0', + 'commission' => 'nullable|numeric|min:0', + 'description' => 'nullable|string', + 'status' => 'required|in:pending,approved,rejected,paid', + ]); + + SalesRecord::create($validated); + + return redirect()->route('sales.records.index') + ->with('success', '실적이 등록되었습니다.'); + } + + /** + * 상세 페이지 + */ + public function show(int $id): View + { + $record = SalesRecord::with(['manager', 'prospect'])->findOrFail($id); + + return view('sales.records.show', compact('record')); + } + + /** + * 수정 폼 + */ + public function edit(int $id): View + { + $record = SalesRecord::findOrFail($id); + $managers = SalesManager::active()->orderBy('name')->get(); + $prospects = SalesProspect::orderBy('company_name')->get(); + + return view('sales.records.edit', compact('record', 'managers', 'prospects')); + } + + /** + * 수정 처리 + */ + public function update(Request $request, int $id) + { + $record = SalesRecord::findOrFail($id); + + $validated = $request->validate([ + 'manager_id' => 'required|exists:sales_managers,id', + 'prospect_id' => 'nullable|exists:sales_prospects,id', + 'record_date' => 'required|date', + 'record_type' => 'required|string|max:50', + 'amount' => 'required|numeric|min:0', + 'commission' => 'nullable|numeric|min:0', + 'description' => 'nullable|string', + 'status' => 'required|in:pending,approved,rejected,paid', + ]); + + $record->update($validated); + + return redirect()->route('sales.records.index') + ->with('success', '실적이 수정되었습니다.'); + } + + /** + * 삭제 처리 + */ + public function destroy(int $id) + { + $record = SalesRecord::findOrFail($id); + $record->delete(); + + return redirect()->route('sales.records.index') + ->with('success', '실적이 삭제되었습니다.'); + } +} diff --git a/app/Models/Sales/SalesManager.php b/app/Models/Sales/SalesManager.php new file mode 100644 index 00000000..4512b0e0 --- /dev/null +++ b/app/Models/Sales/SalesManager.php @@ -0,0 +1,120 @@ + 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * 상위 관리자 + */ + public function parent(): BelongsTo + { + return $this->belongsTo(SalesManager::class, 'parent_id'); + } + + /** + * 하위 관리자 목록 + */ + public function children(): HasMany + { + return $this->hasMany(SalesManager::class, 'parent_id'); + } + + /** + * 등록한 가망고객 + */ + public function registeredProspects(): HasMany + { + return $this->hasMany(SalesProspect::class, 'manager_id'); + } + + /** + * 담당 가망고객 + */ + public function assignedProspects(): HasMany + { + return $this->hasMany(SalesProspect::class, 'sales_manager_id'); + } + + /** + * 영업 실적 + */ + public function records(): HasMany + { + return $this->hasMany(SalesRecord::class, 'manager_id'); + } + + /** + * 상담 기록 + */ + public function consultations(): HasMany + { + return $this->hasMany(SalesProspectConsultation::class, 'manager_id'); + } + + /** + * 역할 라벨 + */ + public function getRoleLabelAttribute(): string + { + return match ($this->role) { + 'operator' => '운영자', + 'sales_admin' => '영업관리', + 'manager' => '매니저', + default => $this->role, + }; + } + + /** + * 역할별 색상 클래스 + */ + public function getRoleColorAttribute(): string + { + return match ($this->role) { + 'operator' => 'bg-purple-100 text-purple-800', + 'sales_admin' => 'bg-blue-100 text-blue-800', + 'manager' => 'bg-green-100 text-green-800', + default => 'bg-gray-100 text-gray-800', + }; + } + + /** + * 활성 관리자만 조회 + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } +} diff --git a/app/Models/Sales/SalesProspect.php b/app/Models/Sales/SalesProspect.php new file mode 100644 index 00000000..77b62036 --- /dev/null +++ b/app/Models/Sales/SalesProspect.php @@ -0,0 +1,139 @@ + 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * 등록한 영업 담당자 + */ + public function manager(): BelongsTo + { + return $this->belongsTo(SalesManager::class, 'manager_id'); + } + + /** + * 담당 매니저 + */ + public function salesManager(): BelongsTo + { + return $this->belongsTo(SalesManager::class, 'sales_manager_id'); + } + + /** + * 계약 상품 목록 + */ + public function products(): HasMany + { + return $this->hasMany(SalesProspectProduct::class, 'prospect_id'); + } + + /** + * 시나리오 체크리스트 + */ + public function scenarios(): HasMany + { + return $this->hasMany(SalesProspectScenario::class, 'prospect_id'); + } + + /** + * 상담 기록 + */ + public function consultations(): HasMany + { + return $this->hasMany(SalesProspectConsultation::class, 'prospect_id'); + } + + /** + * 영업 실적 + */ + public function records(): HasMany + { + return $this->hasMany(SalesRecord::class, 'prospect_id'); + } + + /** + * 상태 라벨 + */ + public function getStatusLabelAttribute(): string + { + return match ($this->status) { + 'lead' => '리드', + 'prospect' => '가망', + 'negotiation' => '협상중', + 'contracted' => '계약완료', + 'lost' => '실패', + default => $this->status, + }; + } + + /** + * 상태별 색상 클래스 + */ + public function getStatusColorAttribute(): string + { + return match ($this->status) { + 'lead' => 'bg-gray-100 text-gray-800', + 'prospect' => 'bg-blue-100 text-blue-800', + 'negotiation' => 'bg-yellow-100 text-yellow-800', + 'contracted' => 'bg-green-100 text-green-800', + 'lost' => 'bg-red-100 text-red-800', + default => 'bg-gray-100 text-gray-800', + }; + } + + /** + * 사업자번호 포맷팅 (XXX-XX-XXXXX) + */ + public function getFormattedBusinessNoAttribute(): string + { + $bizNo = preg_replace('/[^0-9]/', '', $this->business_no); + if (strlen($bizNo) === 10) { + return substr($bizNo, 0, 3) . '-' . substr($bizNo, 3, 2) . '-' . substr($bizNo, 5); + } + return $this->business_no ?? ''; + } + + /** + * 총 계약금액 + */ + public function getTotalContractAmountAttribute(): float + { + return $this->products()->sum('contract_amount'); + } + + /** + * 총 수수료 + */ + public function getTotalCommissionAttribute(): float + { + return $this->products()->sum('commission_amount'); + } +} diff --git a/app/Models/Sales/SalesProspectConsultation.php b/app/Models/Sales/SalesProspectConsultation.php new file mode 100644 index 00000000..639cbde9 --- /dev/null +++ b/app/Models/Sales/SalesProspectConsultation.php @@ -0,0 +1,91 @@ + 'integer', + 'attachment_paths' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * 가망고객 + */ + public function prospect(): BelongsTo + { + return $this->belongsTo(SalesProspect::class, 'prospect_id'); + } + + /** + * 작성자 + */ + public function manager(): BelongsTo + { + return $this->belongsTo(SalesManager::class, 'manager_id'); + } + + /** + * 상담 유형 라벨 + */ + public function getConsultationTypeLabelAttribute(): string + { + return match ($this->consultation_type) { + 'text' => '텍스트', + 'audio' => '음성', + 'file' => '파일', + default => $this->consultation_type, + }; + } + + /** + * 상담 유형별 아이콘 클래스 + */ + public function getConsultationTypeIconAttribute(): string + { + return match ($this->consultation_type) { + 'text' => 'fa-comment', + 'audio' => 'fa-microphone', + 'file' => 'fa-paperclip', + default => 'fa-question', + }; + } + + /** + * 첨부파일 개수 + */ + public function getAttachmentCountAttribute(): int + { + return is_array($this->attachment_paths) ? count($this->attachment_paths) : 0; + } + + /** + * 음성 파일 존재 여부 + */ + public function getHasAudioAttribute(): bool + { + return !empty($this->audio_file_path); + } +} diff --git a/app/Models/Sales/SalesProspectProduct.php b/app/Models/Sales/SalesProspectProduct.php new file mode 100644 index 00000000..2743c182 --- /dev/null +++ b/app/Models/Sales/SalesProspectProduct.php @@ -0,0 +1,97 @@ + 'decimal:2', + 'subscription_fee' => 'decimal:2', + 'commission_rate' => 'decimal:2', + 'commission_amount' => 'decimal:2', + 'payout_rate' => 'decimal:2', + 'payout_amount' => 'decimal:2', + 'contract_date' => 'date', + 'join_approved' => 'boolean', + 'payment_approved' => 'boolean', + 'sub_models' => 'array', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * 가망고객 + */ + public function prospect(): BelongsTo + { + return $this->belongsTo(SalesProspect::class, 'prospect_id'); + } + + /** + * 수수료 자동 계산 + */ + public function calculateCommission(): void + { + $this->commission_amount = $this->contract_amount * ($this->commission_rate / 100); + } + + /** + * 지급금액 계산 + */ + public function calculatePayout(): void + { + $this->payout_amount = $this->contract_amount * ($this->payout_rate / 100); + } + + /** + * 승인 상태 라벨 + */ + public function getApprovalStatusAttribute(): string + { + if ($this->payment_approved) { + return '결제승인'; + } + if ($this->join_approved) { + return '가입승인'; + } + return '대기중'; + } + + /** + * 승인 상태별 색상 + */ + public function getApprovalColorAttribute(): string + { + if ($this->payment_approved) { + return 'bg-green-100 text-green-800'; + } + if ($this->join_approved) { + return 'bg-blue-100 text-blue-800'; + } + return 'bg-gray-100 text-gray-800'; + } +} diff --git a/app/Models/Sales/SalesProspectScenario.php b/app/Models/Sales/SalesProspectScenario.php new file mode 100644 index 00000000..e0e7a2df --- /dev/null +++ b/app/Models/Sales/SalesProspectScenario.php @@ -0,0 +1,68 @@ + 'integer', + 'checkpoint_index' => 'integer', + 'is_checked' => 'boolean', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * 가망고객 + */ + public function prospect(): BelongsTo + { + return $this->belongsTo(SalesProspect::class, 'prospect_id'); + } + + /** + * 시나리오 유형 라벨 + */ + public function getScenarioTypeLabelAttribute(): string + { + return match ($this->scenario_type) { + 'sales' => '영업 시나리오', + 'manager' => '매니저 시나리오', + default => $this->scenario_type, + }; + } + + /** + * 특정 가망고객의 시나리오 진행률 계산 + */ + public static function getProgressRate(int $prospectId, string $scenarioType): float + { + $total = self::where('prospect_id', $prospectId) + ->where('scenario_type', $scenarioType) + ->count(); + + if ($total === 0) { + return 0; + } + + $checked = self::where('prospect_id', $prospectId) + ->where('scenario_type', $scenarioType) + ->where('is_checked', true) + ->count(); + + return round(($checked / $total) * 100, 1); + } +} diff --git a/app/Models/Sales/SalesRecord.php b/app/Models/Sales/SalesRecord.php new file mode 100644 index 00000000..f3a406c5 --- /dev/null +++ b/app/Models/Sales/SalesRecord.php @@ -0,0 +1,117 @@ + 'date', + 'amount' => 'decimal:2', + 'commission' => 'decimal:2', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + 'deleted_at' => 'datetime', + ]; + + /** + * 담당자 + */ + public function manager(): BelongsTo + { + return $this->belongsTo(SalesManager::class, 'manager_id'); + } + + /** + * 가망고객 + */ + public function prospect(): BelongsTo + { + return $this->belongsTo(SalesProspect::class, 'prospect_id'); + } + + /** + * 상태 라벨 + */ + public function getStatusLabelAttribute(): string + { + return match ($this->status) { + 'pending' => '대기', + 'approved' => '승인', + 'rejected' => '반려', + 'paid' => '지급완료', + default => $this->status, + }; + } + + /** + * 상태별 색상 클래스 + */ + public function getStatusColorAttribute(): string + { + return match ($this->status) { + 'pending' => 'bg-yellow-100 text-yellow-800', + 'approved' => 'bg-blue-100 text-blue-800', + 'rejected' => 'bg-red-100 text-red-800', + 'paid' => 'bg-green-100 text-green-800', + default => 'bg-gray-100 text-gray-800', + }; + } + + /** + * 특정 기간의 실적 합계 + */ + public static function getSummary(int $managerId, ?string $startDate = null, ?string $endDate = null): array + { + $query = self::where('manager_id', $managerId); + + if ($startDate) { + $query->where('record_date', '>=', $startDate); + } + if ($endDate) { + $query->where('record_date', '<=', $endDate); + } + + return [ + 'total_amount' => $query->sum('amount'), + 'total_commission' => $query->sum('commission'), + 'record_count' => $query->count(), + 'approved_amount' => (clone $query)->where('status', 'approved')->sum('amount'), + 'paid_amount' => (clone $query)->where('status', 'paid')->sum('amount'), + ]; + } + + /** + * 대기 상태만 조회 + */ + public function scopePending($query) + { + return $query->where('status', 'pending'); + } + + /** + * 승인 상태만 조회 + */ + public function scopeApproved($query) + { + return $query->where('status', 'approved'); + } +} diff --git a/database/migrations/2026_01_26_010000_create_sales_managers_table.php b/database/migrations/2026_01_26_010000_create_sales_managers_table.php new file mode 100644 index 00000000..ea1d55b9 --- /dev/null +++ b/database/migrations/2026_01_26_010000_create_sales_managers_table.php @@ -0,0 +1,41 @@ +id(); + $table->string('member_id', 50)->unique()->comment('로그인 ID'); + $table->string('password', 255)->comment('비밀번호'); + $table->string('name', 100)->comment('성명'); + $table->string('phone', 20)->nullable()->comment('전화번호'); + $table->string('email', 100)->nullable()->comment('이메일'); + $table->foreignId('parent_id')->nullable()->comment('상위 관리자 ID'); + $table->enum('role', ['operator', 'sales_admin', 'manager'])->default('manager')->comment('역할'); + $table->text('remarks')->nullable()->comment('비고'); + $table->boolean('is_active')->default(true)->comment('활성화 여부'); + $table->timestamps(); + $table->softDeletes(); + + $table->index('parent_id'); + $table->index('role'); + $table->index('is_active'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sales_managers'); + } +}; diff --git a/database/migrations/2026_01_26_010100_create_sales_prospects_table.php b/database/migrations/2026_01_26_010100_create_sales_prospects_table.php new file mode 100644 index 00000000..b5afc864 --- /dev/null +++ b/database/migrations/2026_01_26_010100_create_sales_prospects_table.php @@ -0,0 +1,42 @@ +id(); + $table->foreignId('manager_id')->constrained('sales_managers')->cascadeOnDelete()->comment('영업한 영업파트너 ID'); + $table->foreignId('sales_manager_id')->nullable()->constrained('sales_managers')->nullOnDelete()->comment('매칭된 매니저 ID'); + $table->string('company_name', 200)->comment('업체명'); + $table->string('representative', 100)->nullable()->comment('대표자명'); + $table->string('business_no', 20)->nullable()->comment('사업자번호'); + $table->string('contact_phone', 20)->nullable()->comment('연락처'); + $table->string('email', 100)->nullable()->comment('이메일'); + $table->string('address', 500)->nullable()->comment('주소'); + $table->enum('status', ['lead', 'prospect', 'negotiation', 'contracted', 'lost'])->default('lead')->comment('상태'); + $table->timestamps(); + $table->softDeletes(); + + $table->index('manager_id'); + $table->index('sales_manager_id'); + $table->index('status'); + $table->index('business_no'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sales_prospects'); + } +}; diff --git a/database/migrations/2026_01_26_010200_create_sales_prospect_products_table.php b/database/migrations/2026_01_26_010200_create_sales_prospect_products_table.php new file mode 100644 index 00000000..3a67f333 --- /dev/null +++ b/database/migrations/2026_01_26_010200_create_sales_prospect_products_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignId('prospect_id')->constrained('sales_prospects')->cascadeOnDelete(); + $table->string('product_name', 200)->comment('상품명'); + $table->decimal('contract_amount', 15, 2)->default(0)->comment('계약금액'); + $table->decimal('subscription_fee', 15, 2)->default(0)->comment('구독료'); + $table->decimal('commission_rate', 5, 2)->default(20)->comment('수수료율'); + $table->decimal('commission_amount', 15, 2)->default(0)->comment('수수료'); + $table->date('contract_date')->nullable()->comment('계약일'); + $table->boolean('join_approved')->default(false)->comment('가입 승인 여부'); + $table->boolean('payment_approved')->default(false)->comment('결제 승인 여부'); + $table->decimal('payout_rate', 5, 2)->default(0)->comment('지급율'); + $table->decimal('payout_amount', 15, 2)->default(0)->comment('지급금액'); + $table->json('sub_models')->nullable()->comment('선택모델 목록'); + $table->timestamps(); + $table->softDeletes(); + + $table->index('prospect_id'); + $table->index('contract_date'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sales_prospect_products'); + } +}; diff --git a/database/migrations/2026_01_26_010300_create_sales_prospect_scenarios_table.php b/database/migrations/2026_01_26_010300_create_sales_prospect_scenarios_table.php new file mode 100644 index 00000000..ee8fdab7 --- /dev/null +++ b/database/migrations/2026_01_26_010300_create_sales_prospect_scenarios_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('prospect_id')->constrained('sales_prospects')->cascadeOnDelete(); + $table->enum('scenario_type', ['sales', 'manager'])->default('manager')->comment('시나리오 유형'); + $table->unsignedInteger('step_id')->comment('단계 ID'); + $table->unsignedInteger('checkpoint_index')->comment('체크포인트 인덱스'); + $table->boolean('is_checked')->default(false)->comment('체크 여부'); + $table->timestamps(); + + $table->unique(['prospect_id', 'scenario_type', 'step_id', 'checkpoint_index'], 'unique_prospect_scenario_step'); + $table->index('prospect_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sales_prospect_scenarios'); + } +}; diff --git a/database/migrations/2026_01_26_010400_create_sales_prospect_consultations_table.php b/database/migrations/2026_01_26_010400_create_sales_prospect_consultations_table.php new file mode 100644 index 00000000..8f7763b8 --- /dev/null +++ b/database/migrations/2026_01_26_010400_create_sales_prospect_consultations_table.php @@ -0,0 +1,40 @@ +id(); + $table->foreignId('prospect_id')->constrained('sales_prospects')->cascadeOnDelete(); + $table->foreignId('manager_id')->constrained('sales_managers')->cascadeOnDelete(); + $table->enum('scenario_type', ['sales', 'manager'])->default('manager')->comment('시나리오 유형'); + $table->unsignedInteger('step_id')->nullable()->comment('단계 ID'); + $table->text('log_text')->comment('상담 내용'); + $table->string('audio_file_path', 500)->nullable()->comment('음성 파일 경로'); + $table->json('attachment_paths')->nullable()->comment('첨부파일 경로 목록'); + $table->enum('consultation_type', ['text', 'audio', 'file'])->default('text')->comment('상담 유형'); + $table->timestamps(); + $table->softDeletes(); + + $table->index('prospect_id'); + $table->index('manager_id'); + $table->index('scenario_type'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sales_prospect_consultations'); + } +}; diff --git a/database/migrations/2026_01_26_010500_create_sales_records_table.php b/database/migrations/2026_01_26_010500_create_sales_records_table.php new file mode 100644 index 00000000..f30d133e --- /dev/null +++ b/database/migrations/2026_01_26_010500_create_sales_records_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignId('manager_id')->constrained('sales_managers')->cascadeOnDelete()->comment('담당자 ID'); + $table->foreignId('prospect_id')->nullable()->constrained('sales_prospects')->nullOnDelete()->comment('가망고객 ID'); + $table->date('record_date')->comment('실적 일자'); + $table->string('record_type', 50)->comment('실적 유형'); + $table->decimal('amount', 15, 2)->default(0)->comment('실적 금액'); + $table->decimal('commission', 15, 2)->default(0)->comment('수수료'); + $table->text('description')->nullable()->comment('설명'); + $table->enum('status', ['pending', 'approved', 'rejected', 'paid'])->default('pending')->comment('상태'); + $table->timestamps(); + $table->softDeletes(); + + $table->index('manager_id'); + $table->index('prospect_id'); + $table->index('record_date'); + $table->index('status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('sales_records'); + } +}; diff --git a/resources/views/sales/managers/create.blade.php b/resources/views/sales/managers/create.blade.php new file mode 100644 index 00000000..bde2c0ac --- /dev/null +++ b/resources/views/sales/managers/create.blade.php @@ -0,0 +1,111 @@ +@extends('layouts.app') + +@section('title', '영업담당자 등록') + +@section('content') +
영업 담당자 및 매니저를 관리합니다
+| 이름 | +아이디 | +역할 | +연락처 | +상위관리자 | +등록일 | +관리 | +
|---|---|---|---|---|---|---|
|
+ {{ $manager->name }}
+ @if($manager->email)
+ {{ $manager->email }}
+ @endif
+ |
+ + {{ $manager->member_id }} + | ++ + {{ $manager->role_label }} + + | ++ {{ $manager->phone ?? '-' }} + | ++ {{ $manager->parent?->name ?? '-' }} + | ++ {{ $manager->created_at->format('Y-m-d') }} + | ++ 상세 + 수정 + + | +
| + 등록된 담당자가 없습니다. + | +||||||
{{ $manager->remarks }}
+| 이름 | +역할 | +연락처 | +
|---|---|---|
| + + {{ $child->name }} + + | ++ + {{ $child->role_label }} + + | +{{ $child->phone ?? '-' }} | +
영업 가망고객을 관리합니다
+| 업체정보 | +상태 | +담당자 | +연락처 | +계약금액 | +등록일 | +관리 | +
|---|---|---|---|---|---|---|
|
+ {{ $prospect->company_name }}
+ @if($prospect->business_no)
+ {{ $prospect->formatted_business_no }}
+ @endif
+ @if($prospect->representative)
+ {{ $prospect->representative }}
+ @endif
+ |
+ + + {{ $prospect->status_label }} + + | +
+ {{ $prospect->manager?->name ?? '-' }}
+ @if($prospect->salesManager && $prospect->salesManager->id !== $prospect->manager?->id)
+ 담당: {{ $prospect->salesManager->name }}
+ @endif
+ |
+ + {{ $prospect->contact_phone ?? '-' }} + | +
+ @if($prospect->products->count() > 0)
+ {{ number_format($prospect->total_contract_amount) }}원
+ 수수료: {{ number_format($prospect->total_commission) }}원
+ @else
+ -
+ @endif
+ |
+ + {{ $prospect->created_at->format('Y-m-d') }} + | ++ 상세 + 수정 + + | +
| + 등록된 가망고객이 없습니다. + | +||||||
+ + {{ $prospect->status_label }} + + @if($prospect->business_no) + {{ $prospect->formatted_business_no }} + @endif +
+| 상품명 | +계약금액 | +수수료 | +승인상태 | +계약일 | +
|---|---|---|---|---|
| {{ $product->product_name }} | +{{ number_format($product->contract_amount) }}원 | +{{ number_format($product->commission_amount) }}원 | ++ + {{ $product->approval_status }} + + | +{{ $product->contract_date?->format('Y-m-d') ?? '-' }} | +
등록된 계약 상품이 없습니다.
+ @endif +{{ $consultation->log_text }}
+등록된 상담 기록이 없습니다.
+ @endif +영업 실적을 관리합니다
+| 실적일 | +담당자 | +유형 | +가망고객 | +금액 | +수수료 | +상태 | +관리 | +
|---|---|---|---|---|---|---|---|
| + {{ $record->record_date->format('Y-m-d') }} + | +
+ {{ $record->manager?->name ?? '-' }}
+ |
+ + {{ $record->record_type }} + | ++ @if($record->prospect) + + {{ $record->prospect->company_name }} + + @else + - + @endif + | ++ {{ number_format($record->amount) }}원 + | ++ {{ number_format($record->commission) }}원 + | ++ + {{ $record->status_label }} + + | ++ 상세 + 수정 + + | +
| + 등록된 실적이 없습니다. + | +|||||||
{{ $record->description }}
+