with(['user', 'department']) ->forTenant($tenantId); if (! empty($filters['q'])) { $search = $filters['q']; $query->where(function ($q) use ($search) { $q->where('display_name', 'like', "%{$search}%") ->orWhereHas('user', function ($uq) use ($search) { $uq->where('name', 'like', "%{$search}%") ->orWhere('email', 'like', "%{$search}%") ->orWhere('phone', 'like', "%{$search}%"); }); }); } if (! empty($filters['status'])) { $query->where('employee_status', $filters['status']); } if (! empty($filters['department_id'])) { $query->where('department_id', $filters['department_id']); } $sortBy = $filters['sort_by'] ?? 'hire_date_asc'; switch ($sortBy) { case 'hire_date_asc': $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) ASC"); break; case 'hire_date_desc': $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) DESC"); break; case 'resign_date_asc': $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.resign_date')) ASC"); break; case 'resign_date_desc': $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.resign_date')) DESC"); break; default: $query->orderByRaw("FIELD(employee_status, 'active', 'leave', 'resigned')") ->orderBy('created_at', 'desc'); break; } return $query->paginate($perPage); } /** * 사업소득자 상세 조회 */ public function getById(int $id): ?BusinessIncomeEarner { $tenantId = session('selected_tenant_id'); return BusinessIncomeEarner::query() ->with(['user', 'department', 'manager']) ->forTenant($tenantId) ->find($id); } /** * 사업소득자 통계 */ public function getStats(): array { $tenantId = session('selected_tenant_id'); $baseQuery = BusinessIncomeEarner::query()->forTenant($tenantId); return [ 'total' => (clone $baseQuery)->count(), 'active' => (clone $baseQuery)->where('employee_status', 'active')->count(), 'leave' => (clone $baseQuery)->where('employee_status', 'leave')->count(), 'resigned' => (clone $baseQuery)->where('employee_status', 'resigned')->count(), ]; } /** * 테넌트 소속이지만 사업소득자 미등록인 사용자 검색 */ public function searchTenantUsers(string $query): array { $tenantId = session('selected_tenant_id'); $builder = User::query() ->select('users.id', 'users.name', 'users.email', 'users.phone') ->join('user_tenants as ut', function ($join) use ($tenantId) { $join->on('users.id', '=', 'ut.user_id') ->where('ut.tenant_id', $tenantId) ->whereNull('ut.deleted_at'); }) ->leftJoin('tenant_user_profiles as tup', function ($join) use ($tenantId) { $join->on('users.id', '=', 'tup.user_id') ->where('tup.tenant_id', $tenantId); }) ->whereNull('tup.id') ->whereNull('users.deleted_at'); if ($query !== '') { $like = "%{$query}%"; $builder->where(function ($q) use ($like) { $q->where('users.name', 'like', $like) ->orWhere('users.email', 'like', $like) ->orWhere('users.phone', 'like', $like); }); } return $builder->orderBy('users.name')->limit(20)->get()->toArray(); } /** * 사업소득자 등록 (User is_active=false + TenantUserProfile 생성) */ public function create(array $data): BusinessIncomeEarner { $tenantId = session('selected_tenant_id'); return DB::transaction(function () use ($data, $tenantId) { if (! empty($data['existing_user_id'])) { $user = User::findOrFail($data['existing_user_id']); $isMember = $user->tenants() ->wherePivot('tenant_id', $tenantId) ->wherePivotNull('deleted_at') ->exists(); if (! $isMember) { throw new \RuntimeException('해당 사용자는 현재 테넌트에 소속되어 있지 않습니다.'); } $alreadyRegistered = BusinessIncomeEarner::where('tenant_id', $tenantId) ->where('user_id', $user->id) ->exists(); if ($alreadyRegistered) { throw new \RuntimeException('이미 사업소득자로 등록된 사용자입니다.'); } } else { // 신규 사용자 생성 (is_active=false, 로그인 불가) $loginId = ! empty($data['email']) ? Str::before($data['email'], '@') : 'BIZ_'.strtolower(Str::random(6)); while (User::where('user_id', $loginId)->exists()) { $loginId = $loginId.'_'.Str::random(3); } $email = ! empty($data['email']) ? $data['email'] : $loginId.'@placeholder.local'; while (User::where('email', $email)->exists()) { $email = $loginId.'_'.Str::random(3).'@placeholder.local'; } $user = User::create([ 'user_id' => $loginId, 'name' => $data['name'], 'email' => $email, 'phone' => $data['phone'] ?? null, 'password' => Hash::make(Str::random(32)), 'role' => 'ops', 'is_active' => false, 'must_change_password' => false, 'created_by' => auth()->id(), ]); if ($tenantId) { $user->tenants()->attach($tenantId, [ 'is_active' => true, 'is_default' => true, 'joined_at' => now(), ]); } } // json_extra 구성 $jsonExtra = []; $scalarKeys = [ 'hire_date', 'resign_date', 'address', 'emergency_contact', 'resident_number', 'business_registration_number', 'business_name', 'business_representative', 'business_type', 'business_category', 'business_address', ]; foreach ($scalarKeys as $key) { if (! empty($data[$key])) { $jsonExtra[$key] = $data[$key]; } } // 급여이체정보 if (! empty($data['bank_account']) && is_array($data['bank_account'])) { $bankAccount = array_filter($data['bank_account'], fn ($v) => $v !== null && $v !== ''); if (! empty($bankAccount)) { $jsonExtra['bank_account'] = $bankAccount; } } // 부양가족 정보 if (! empty($data['dependents']) && is_array($data['dependents'])) { $dependents = array_values(array_filter($data['dependents'], function ($dep) { return ! empty($dep['name']); })); $dependents = array_map(function ($dep) { $dep['is_disabled'] = filter_var($dep['is_disabled'] ?? false, FILTER_VALIDATE_BOOLEAN); $dep['is_dependent'] = filter_var($dep['is_dependent'] ?? false, FILTER_VALIDATE_BOOLEAN); return $dep; }, $dependents); if (! empty($dependents)) { $jsonExtra['dependents'] = $dependents; } } $earner = BusinessIncomeEarner::create([ 'tenant_id' => $tenantId, 'user_id' => $user->id, 'department_id' => $data['department_id'] ?? null, 'position_key' => $data['position_key'] ?? null, 'job_title_key' => $data['job_title_key'] ?? null, 'work_location_key' => $data['work_location_key'] ?? null, 'employment_type_key' => $data['employment_type_key'] ?? null, 'employee_status' => $data['employee_status'] ?? 'active', 'manager_user_id' => $data['manager_user_id'] ?? null, 'display_name' => $data['display_name'] ?? $data['name'], 'json_extra' => ! empty($jsonExtra) ? $jsonExtra : null, ]); return $earner->load(['user', 'department']); }); } /** * 사업소득자 정보 수정 */ public function update(int $id, array $data): ?BusinessIncomeEarner { $earner = $this->getById($id); if (! $earner) { return null; } $updateData = array_filter([ 'department_id' => $data['department_id'] ?? null, 'position_key' => $data['position_key'] ?? null, 'job_title_key' => $data['job_title_key'] ?? null, 'work_location_key' => $data['work_location_key'] ?? null, 'employment_type_key' => $data['employment_type_key'] ?? null, 'employee_status' => $data['employee_status'] ?? null, 'manager_user_id' => $data['manager_user_id'] ?? null, 'display_name' => $data['display_name'] ?? null, ], fn ($v) => $v !== null); // json_extra 업데이트 $jsonExtraKeys = [ 'hire_date', 'resign_date', 'address', 'emergency_contact', 'salary', 'resident_number', 'business_registration_number', 'business_name', 'business_representative', 'business_type', 'business_category', 'business_address', ]; $extra = $earner->json_extra ?? []; foreach ($jsonExtraKeys as $key) { if (array_key_exists($key, $data)) { if ($data[$key] === null || $data[$key] === '') { unset($extra[$key]); } else { $extra[$key] = $data[$key]; } } } // 급여이체정보 if (array_key_exists('bank_account', $data)) { if (! empty($data['bank_account']) && is_array($data['bank_account'])) { $bankAccount = array_filter($data['bank_account'], fn ($v) => $v !== null && $v !== ''); if (! empty($bankAccount)) { $extra['bank_account'] = $bankAccount; } else { unset($extra['bank_account']); } } else { unset($extra['bank_account']); } } // 부양가족 정보 if (array_key_exists('dependents', $data)) { if (! empty($data['dependents']) && is_array($data['dependents'])) { $dependents = array_values(array_filter($data['dependents'], function ($dep) { return ! empty($dep['name']); })); $dependents = array_map(function ($dep) { $dep['is_disabled'] = filter_var($dep['is_disabled'] ?? false, FILTER_VALIDATE_BOOLEAN); $dep['is_dependent'] = filter_var($dep['is_dependent'] ?? false, FILTER_VALIDATE_BOOLEAN); return $dep; }, $dependents); if (! empty($dependents)) { $extra['dependents'] = $dependents; } else { unset($extra['dependents']); } } else { unset($extra['dependents']); } } $updateData['json_extra'] = ! empty($extra) ? $extra : null; $earner->update($updateData); // User 기본정보 동기화 if ($earner->user) { $userUpdate = []; if (! empty($data['name'])) { $userUpdate['name'] = $data['name']; } if (! empty($data['email'])) { $userUpdate['email'] = $data['email']; } if (! empty($data['phone'])) { $userUpdate['phone'] = $data['phone']; } if (! empty($userUpdate)) { $earner->user->update($userUpdate); } } return $earner->fresh(['user', 'department']); } /** * 사업소득자 삭제 (퇴직 처리) */ public function delete(int $id): bool { $earner = $this->getById($id); if (! $earner) { return false; } $earner->update(['employee_status' => 'resigned']); return true; } /** * 부서 목록 (드롭다운용) */ public function getDepartments(): \Illuminate\Database\Eloquent\Collection { $tenantId = session('selected_tenant_id'); return Department::query() ->where('is_active', true) ->when($tenantId, fn ($q) => $q->where('tenant_id', $tenantId)) ->orderBy('sort_order') ->orderBy('name') ->get(['id', 'name', 'code']); } /** * 직급/직책 목록 (드롭다운용) */ public function getPositions(string $type = 'rank'): \Illuminate\Database\Eloquent\Collection { return Position::query() ->forTenant() ->where('type', $type) ->where('is_active', true) ->ordered() ->get(['id', 'key', 'name']); } }