- SalesManagerController: User 시스템 기반으로 재구현 - SalesManagerService: 영업담당자 CRUD, 승인/반려 로직 - SalesManagerDocument: 멀티파일 업로드 모델 - User 모델에 parent, approval 관계 및 메서드 추가 - SalesRoleSeeder: 영업 역할 시더 (sales_operator, sales_admin, sales_manager) - 뷰 파일 전면 수정 (역할 체크박스, 멀티파일 업로드, 승인/반려 UI) - 라우트 추가 (approve, reject, documents) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
345 lines
11 KiB
PHP
345 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Sales;
|
|
|
|
use App\Models\DepartmentUser;
|
|
use App\Models\Sales\SalesManagerDocument;
|
|
use App\Models\User;
|
|
use App\Models\UserRole;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
class SalesManagerService
|
|
{
|
|
/**
|
|
* 영업담당자 생성
|
|
*/
|
|
public function createSalesManager(array $data, array $documents = []): User
|
|
{
|
|
return DB::transaction(function () use ($data, $documents) {
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
// 1. 사용자 생성
|
|
$user = User::create([
|
|
'user_id' => $data['user_id'] ?? null,
|
|
'name' => $data['name'],
|
|
'email' => $data['email'],
|
|
'phone' => $data['phone'] ?? null,
|
|
'password' => Hash::make($data['password']),
|
|
'is_active' => false, // 승인 전까지 비활성
|
|
'parent_id' => $data['parent_id'] ?? null,
|
|
'approval_status' => 'pending', // 승인 대기 상태
|
|
'must_change_password' => true,
|
|
]);
|
|
|
|
// 2. 테넌트 연결
|
|
$user->tenants()->attach($tenantId, [
|
|
'is_active' => true,
|
|
'is_default' => true,
|
|
'joined_at' => now(),
|
|
]);
|
|
|
|
// 3. 역할 할당
|
|
if (!empty($data['role_ids'])) {
|
|
$this->syncRoles($user, $tenantId, $data['role_ids']);
|
|
}
|
|
|
|
// 4. 부서 할당
|
|
if (!empty($data['department_ids'])) {
|
|
$this->syncDepartments($user, $tenantId, $data['department_ids']);
|
|
}
|
|
|
|
// 5. 첨부 서류 저장
|
|
if (!empty($documents)) {
|
|
$this->uploadDocuments($user, $tenantId, $documents);
|
|
}
|
|
|
|
return $user;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 영업담당자 수정
|
|
*/
|
|
public function updateSalesManager(User $user, array $data, array $documents = []): User
|
|
{
|
|
return DB::transaction(function () use ($user, $data, $documents) {
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
// 1. 기본 정보 업데이트
|
|
$updateData = [
|
|
'name' => $data['name'],
|
|
'email' => $data['email'],
|
|
'phone' => $data['phone'] ?? null,
|
|
'parent_id' => $data['parent_id'] ?? null,
|
|
];
|
|
|
|
// 비밀번호 변경 시에만 업데이트
|
|
if (!empty($data['password'])) {
|
|
$updateData['password'] = Hash::make($data['password']);
|
|
}
|
|
|
|
$user->update($updateData);
|
|
|
|
// 2. 역할 동기화
|
|
if (isset($data['role_ids'])) {
|
|
$this->syncRoles($user, $tenantId, $data['role_ids']);
|
|
}
|
|
|
|
// 3. 부서 동기화
|
|
if (isset($data['department_ids'])) {
|
|
$this->syncDepartments($user, $tenantId, $data['department_ids']);
|
|
}
|
|
|
|
// 4. 새 첨부 서류 저장
|
|
if (!empty($documents)) {
|
|
$this->uploadDocuments($user, $tenantId, $documents);
|
|
}
|
|
|
|
return $user->fresh();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 영업담당자 승인
|
|
*/
|
|
public function approve(User $user, int $approverId): User
|
|
{
|
|
$user->update([
|
|
'approval_status' => 'approved',
|
|
'approved_by' => $approverId,
|
|
'approved_at' => now(),
|
|
'is_active' => true,
|
|
'rejection_reason' => null,
|
|
]);
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* 영업담당자 반려
|
|
*/
|
|
public function reject(User $user, int $approverId, string $reason): User
|
|
{
|
|
$user->update([
|
|
'approval_status' => 'rejected',
|
|
'approved_by' => $approverId,
|
|
'approved_at' => now(),
|
|
'rejection_reason' => $reason,
|
|
'is_active' => false,
|
|
]);
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* 역할 동기화
|
|
*/
|
|
public function syncRoles(User $user, int $tenantId, array $roleIds): void
|
|
{
|
|
// 기존 역할 삭제
|
|
UserRole::withTrashed()
|
|
->where('user_id', $user->id)
|
|
->where('tenant_id', $tenantId)
|
|
->forceDelete();
|
|
|
|
// 새 역할 추가
|
|
foreach ($roleIds as $roleId) {
|
|
UserRole::create([
|
|
'user_id' => $user->id,
|
|
'tenant_id' => $tenantId,
|
|
'role_id' => $roleId,
|
|
'assigned_at' => now(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 부서 동기화
|
|
*/
|
|
public function syncDepartments(User $user, int $tenantId, array $departmentIds): void
|
|
{
|
|
// 기존 부서 삭제
|
|
DepartmentUser::withTrashed()
|
|
->where('user_id', $user->id)
|
|
->where('tenant_id', $tenantId)
|
|
->forceDelete();
|
|
|
|
// 새 부서 추가
|
|
foreach ($departmentIds as $index => $departmentId) {
|
|
DepartmentUser::create([
|
|
'user_id' => $user->id,
|
|
'tenant_id' => $tenantId,
|
|
'department_id' => $departmentId,
|
|
'is_primary' => $index === 0,
|
|
'joined_at' => now(),
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 서류 업로드
|
|
*/
|
|
public function uploadDocuments(User $user, int $tenantId, array $documents): array
|
|
{
|
|
$uploaded = [];
|
|
|
|
foreach ($documents as $doc) {
|
|
if (!isset($doc['file']) || !$doc['file'] instanceof UploadedFile) {
|
|
continue;
|
|
}
|
|
|
|
$file = $doc['file'];
|
|
$documentType = $doc['document_type'] ?? 'other';
|
|
$description = $doc['description'] ?? null;
|
|
|
|
// 파일 저장
|
|
$storedName = Str::uuid() . '.' . $file->getClientOriginalExtension();
|
|
$filePath = "sales-managers/{$user->id}/{$storedName}";
|
|
|
|
Storage::disk('tenant')->put($filePath, file_get_contents($file));
|
|
|
|
// DB 저장
|
|
$document = SalesManagerDocument::create([
|
|
'tenant_id' => $tenantId,
|
|
'user_id' => $user->id,
|
|
'file_path' => $filePath,
|
|
'original_name' => $file->getClientOriginalName(),
|
|
'stored_name' => $storedName,
|
|
'mime_type' => $file->getMimeType(),
|
|
'file_size' => $file->getSize(),
|
|
'document_type' => $documentType,
|
|
'description' => $description,
|
|
'uploaded_by' => auth()->id(),
|
|
]);
|
|
|
|
$uploaded[] = $document;
|
|
}
|
|
|
|
return $uploaded;
|
|
}
|
|
|
|
/**
|
|
* 서류 삭제
|
|
*/
|
|
public function deleteDocument(SalesManagerDocument $document): bool
|
|
{
|
|
// 물리 파일 삭제
|
|
if ($document->existsInStorage()) {
|
|
Storage::disk('tenant')->delete($document->file_path);
|
|
}
|
|
|
|
// DB 삭제
|
|
$document->deleted_by = auth()->id();
|
|
$document->save();
|
|
|
|
return $document->delete();
|
|
}
|
|
|
|
/**
|
|
* 영업담당자 목록 조회 (역할 기반)
|
|
*/
|
|
public function getSalesManagers(array $filters = [])
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
// 영업관련 역할을 가진 사용자 조회
|
|
$query = User::query()
|
|
->whereHas('userRoles', function ($q) use ($tenantId) {
|
|
$q->where('tenant_id', $tenantId)
|
|
->whereHas('role', function ($rq) {
|
|
$rq->whereIn('name', ['sales_operator', 'sales_admin', 'sales_manager']);
|
|
});
|
|
})
|
|
->with(['parent', 'userRoles.role', 'salesDocuments']);
|
|
|
|
// 검색
|
|
if (!empty($filters['search'])) {
|
|
$search = $filters['search'];
|
|
$query->where(function ($q) use ($search) {
|
|
$q->where('name', 'like', "%{$search}%")
|
|
->orWhere('user_id', 'like', "%{$search}%")
|
|
->orWhere('email', 'like', "%{$search}%")
|
|
->orWhere('phone', 'like', "%{$search}%");
|
|
});
|
|
}
|
|
|
|
// 역할 필터
|
|
if (!empty($filters['role'])) {
|
|
$roleName = $filters['role'];
|
|
$query->whereHas('userRoles', function ($q) use ($tenantId, $roleName) {
|
|
$q->where('tenant_id', $tenantId)
|
|
->whereHas('role', function ($rq) use ($roleName) {
|
|
$rq->where('name', $roleName);
|
|
});
|
|
});
|
|
}
|
|
|
|
// 승인 상태 필터
|
|
if (!empty($filters['approval_status'])) {
|
|
$query->where('approval_status', $filters['approval_status']);
|
|
}
|
|
|
|
return $query->orderBy('name');
|
|
}
|
|
|
|
/**
|
|
* 상위 관리자 목록 (운영자, 영업관리)
|
|
*/
|
|
public function getParentCandidates(?int $excludeId = null)
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
$query = User::query()
|
|
->where('is_active', true)
|
|
->where('approval_status', 'approved')
|
|
->whereHas('userRoles', function ($q) use ($tenantId) {
|
|
$q->where('tenant_id', $tenantId)
|
|
->whereHas('role', function ($rq) {
|
|
$rq->whereIn('name', ['sales_operator', 'sales_admin']);
|
|
});
|
|
});
|
|
|
|
if ($excludeId) {
|
|
$query->where('id', '!=', $excludeId);
|
|
}
|
|
|
|
return $query->orderBy('name')->get();
|
|
}
|
|
|
|
/**
|
|
* 통계 조회
|
|
*/
|
|
public function getStats(): array
|
|
{
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
$baseQuery = User::query()
|
|
->whereHas('userRoles', function ($q) use ($tenantId) {
|
|
$q->where('tenant_id', $tenantId)
|
|
->whereHas('role', function ($rq) {
|
|
$rq->whereIn('name', ['sales_operator', 'sales_admin', 'sales_manager']);
|
|
});
|
|
});
|
|
|
|
return [
|
|
'total' => (clone $baseQuery)->count(),
|
|
'pending' => (clone $baseQuery)->where('approval_status', 'pending')->count(),
|
|
'approved' => (clone $baseQuery)->where('approval_status', 'approved')->count(),
|
|
'operators' => (clone $baseQuery)
|
|
->whereHas('userRoles.role', fn($q) => $q->where('name', 'sales_operator'))
|
|
->count(),
|
|
'sales_admins' => (clone $baseQuery)
|
|
->whereHas('userRoles.role', fn($q) => $q->where('name', 'sales_admin'))
|
|
->count(),
|
|
'managers' => (clone $baseQuery)
|
|
->whereHas('userRoles.role', fn($q) => $q->where('name', 'sales_manager'))
|
|
->count(),
|
|
];
|
|
}
|
|
}
|