- 영업팀 포함 부서 사원 기본 제외 (외부직원) - json_extra.is_excluded 플래그로 강제 제외/복원 토글 - 필터에 '제외 사원 표시' 체크박스 추가 - 제외 사원 시각적 구분 (주황 배경, 제외 뱃지)
690 lines
24 KiB
PHP
690 lines
24 KiB
PHP
<?php
|
|
|
|
namespace App\Services\HR;
|
|
|
|
use App\Models\HR\Employee;
|
|
use App\Models\HR\Position;
|
|
use App\Models\Tenants\Department;
|
|
use App\Models\User;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Str;
|
|
|
|
class EmployeeService
|
|
{
|
|
/**
|
|
* 사원 목록 조회 (페이지네이션)
|
|
*/
|
|
public function getEmployees(array $filters = [], int $perPage = 20): LengthAwarePaginator
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$query = Employee::query()
|
|
->with(['user', 'department'])
|
|
->forTenant($tenantId);
|
|
|
|
// 제외 사원 필터 (기본: 숨김)
|
|
if (empty($filters['show_excluded'])) {
|
|
// "영업팀" 포함 부서 사원 제외
|
|
$query->where(function ($q) {
|
|
$q->whereDoesntHave('department', function ($dq) {
|
|
$dq->where('name', 'like', '%영업팀%');
|
|
})->orWhereNull('department_id');
|
|
});
|
|
// 강제 제외된 사원 제외
|
|
$query->where(function ($q) {
|
|
$q->whereNull('json_extra->is_excluded')
|
|
->orWhere('json_extra->is_excluded', false);
|
|
});
|
|
}
|
|
|
|
// 검색 필터 (이름, 이메일, 연락처)
|
|
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 getEmployeeById(int $id): ?Employee
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
return Employee::query()
|
|
->with(['user', 'department', 'manager'])
|
|
->forTenant($tenantId)
|
|
->find($id);
|
|
}
|
|
|
|
/**
|
|
* 사원 통계
|
|
*/
|
|
public function getStats(bool $showExcluded = false): array
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$baseQuery = Employee::query()->forTenant($tenantId);
|
|
|
|
if (! $showExcluded) {
|
|
$baseQuery->where(function ($q) {
|
|
$q->whereDoesntHave('department', function ($dq) {
|
|
$dq->where('name', 'like', '%영업팀%');
|
|
})->orWhereNull('department_id');
|
|
})->where(function ($q) {
|
|
$q->whereNull('json_extra->is_excluded')
|
|
->orWhere('json_extra->is_excluded', false);
|
|
});
|
|
}
|
|
|
|
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 + TenantUserProfile 동시 생성)
|
|
*/
|
|
public function createEmployee(array $data): Employee
|
|
{
|
|
$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('해당 사용자는 현재 테넌트에 소속되어 있지 않습니다.');
|
|
}
|
|
|
|
// 이미 사원 등록 여부 확인
|
|
$alreadyEmployee = Employee::where('tenant_id', $tenantId)
|
|
->where('user_id', $user->id)
|
|
->exists();
|
|
|
|
if ($alreadyEmployee) {
|
|
throw new \RuntimeException('이미 사원으로 등록된 사용자입니다.');
|
|
}
|
|
} else {
|
|
// 신규 사용자 생성
|
|
$loginId = ! empty($data['email'])
|
|
? Str::before($data['email'], '@')
|
|
: 'EMP_'.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('sam1234!'),
|
|
'role' => 'ops',
|
|
'is_active' => true,
|
|
'must_change_password' => true,
|
|
'created_by' => auth()->id(),
|
|
]);
|
|
|
|
if ($tenantId) {
|
|
$user->tenants()->attach($tenantId, [
|
|
'is_active' => true,
|
|
'is_default' => true,
|
|
'joined_at' => now(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
// json_extra 구성
|
|
$jsonExtra = [];
|
|
foreach (['hire_date', 'resign_date', 'address', 'emergency_contact', 'resident_number'] as $key) {
|
|
if (! empty($data[$key])) {
|
|
$jsonExtra[$key] = $data[$key];
|
|
}
|
|
}
|
|
|
|
// 급여이체정보 (bank_account 객체)
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 부양가족 정보 (dependents 배열)
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Employee(TenantUserProfile) 생성
|
|
$employee = Employee::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 $employee->load(['user', 'department']);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 사원 정보 수정
|
|
*/
|
|
public function updateEmployee(int $id, array $data): ?Employee
|
|
{
|
|
$employee = $this->getEmployeeById($id);
|
|
if (! $employee) {
|
|
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'];
|
|
$extra = $employee->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];
|
|
}
|
|
}
|
|
}
|
|
|
|
// 급여이체정보 (bank_account 객체)
|
|
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']);
|
|
}
|
|
}
|
|
|
|
// 부양가족 정보 (dependents 배열)
|
|
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;
|
|
|
|
$employee->update($updateData);
|
|
|
|
// User 기본정보 동기화
|
|
if ($employee->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)) {
|
|
$employee->user->update($userUpdate);
|
|
}
|
|
}
|
|
|
|
return $employee->fresh(['user', 'department']);
|
|
}
|
|
|
|
/**
|
|
* 사원 삭제 (퇴직 처리)
|
|
*/
|
|
public function deleteEmployee(int $id): bool
|
|
{
|
|
$employee = $this->getEmployeeById($id);
|
|
if (! $employee) {
|
|
return false;
|
|
}
|
|
|
|
$employee->update(['employee_status' => 'resigned']);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 사원 영구 삭제 (퇴직자만, 슈퍼관리자 전용)
|
|
*/
|
|
public function forceDeleteEmployee(int $id): array
|
|
{
|
|
$employee = $this->getEmployeeById($id);
|
|
if (! $employee) {
|
|
return ['success' => false, 'message' => '사원 정보를 찾을 수 없습니다.'];
|
|
}
|
|
|
|
if ($employee->employee_status !== 'resigned') {
|
|
return ['success' => false, 'message' => '퇴직 상태인 사원만 영구삭제할 수 있습니다.'];
|
|
}
|
|
|
|
$name = $employee->display_name ?? $employee->user?->name ?? '알 수 없음';
|
|
|
|
// 관련 첨부파일 삭제
|
|
\App\Models\Boards\File::where('document_type', 'employee_profile')
|
|
->where('document_id', $employee->id)
|
|
->where('tenant_id', session('selected_tenant_id'))
|
|
->delete();
|
|
|
|
// tenant_user_profiles 레코드 영구 삭제
|
|
$employee->delete();
|
|
|
|
return ['success' => true, 'message' => "사원 '{$name}'이(가) 영구삭제되었습니다."];
|
|
}
|
|
|
|
/**
|
|
* 사원 제외/복원 토글
|
|
*/
|
|
public function toggleExclude(int $id): ?Employee
|
|
{
|
|
$employee = $this->getEmployeeById($id);
|
|
if (! $employee) {
|
|
return null;
|
|
}
|
|
|
|
$isExcluded = $employee->getJsonExtraValue('is_excluded', false);
|
|
$employee->setJsonExtraValue('is_excluded', ! $isExcluded);
|
|
$employee->save();
|
|
|
|
return $employee;
|
|
}
|
|
|
|
/**
|
|
* 부서 목록 (드롭다운용)
|
|
*/
|
|
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']);
|
|
}
|
|
|
|
/**
|
|
* 입퇴사자 현황 조회 (페이지네이션)
|
|
*/
|
|
public function getEmployeeTenure(array $filters = [], int $perPage = 50): LengthAwarePaginator
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$query = Employee::query()
|
|
->with(['user', 'department'])
|
|
->forTenant($tenantId)
|
|
->whereNotNull('json_extra->hire_date');
|
|
|
|
// 이름 검색
|
|
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}%");
|
|
});
|
|
});
|
|
}
|
|
|
|
// 부서 필터
|
|
if (! empty($filters['department_id'])) {
|
|
$query->where('department_id', $filters['department_id']);
|
|
}
|
|
|
|
// 상태 필터
|
|
if (! empty($filters['status'])) {
|
|
if ($filters['status'] === 'active') {
|
|
$query->where('employee_status', 'active');
|
|
} elseif ($filters['status'] === 'resigned') {
|
|
$query->where('employee_status', 'resigned');
|
|
}
|
|
}
|
|
|
|
// 입사기간 범위
|
|
if (! empty($filters['hire_from'])) {
|
|
$query->where('json_extra->hire_date', '>=', $filters['hire_from']);
|
|
}
|
|
if (! empty($filters['hire_to'])) {
|
|
$query->where('json_extra->hire_date', '<=', $filters['hire_to']);
|
|
}
|
|
|
|
// 정렬
|
|
$sortBy = $filters['sort_by'] ?? 'hire_date_desc';
|
|
switch ($sortBy) {
|
|
case 'hire_date_asc':
|
|
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) ASC");
|
|
break;
|
|
case 'tenure_desc':
|
|
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) ASC");
|
|
break;
|
|
case 'tenure_asc':
|
|
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) DESC");
|
|
break;
|
|
default: // hire_date_desc
|
|
$query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) DESC");
|
|
break;
|
|
}
|
|
|
|
return $query->paginate($perPage);
|
|
}
|
|
|
|
/**
|
|
* 입퇴사자 통계
|
|
*/
|
|
public function getTenureStats(): array
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$baseQuery = Employee::query()
|
|
->forTenant($tenantId)
|
|
->whereNotNull('json_extra->hire_date');
|
|
|
|
$total = (clone $baseQuery)->count();
|
|
$active = (clone $baseQuery)->where('employee_status', 'active')->count();
|
|
$resigned = (clone $baseQuery)->where('employee_status', 'resigned')->count();
|
|
|
|
// 올해 입사자
|
|
$thisYear = now()->year;
|
|
$hiredThisYear = (clone $baseQuery)
|
|
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) LIKE ?", ["{$thisYear}%"])
|
|
->count();
|
|
|
|
// 올해 퇴사자
|
|
$resignedThisYear = Employee::query()
|
|
->forTenant($tenantId)
|
|
->whereNotNull('json_extra->resign_date')
|
|
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.resign_date')) LIKE ?", ["{$thisYear}%"])
|
|
->count();
|
|
|
|
// 평균 근속기간 (재직자 기준)
|
|
$activeEmployees = (clone $baseQuery)
|
|
->where('employee_status', 'active')
|
|
->get(['json_extra']);
|
|
|
|
$avgTenureDays = 0;
|
|
if ($activeEmployees->isNotEmpty()) {
|
|
$totalDays = 0;
|
|
$count = 0;
|
|
foreach ($activeEmployees as $emp) {
|
|
$hireDate = $emp->json_extra['hire_date'] ?? null;
|
|
if ($hireDate) {
|
|
$totalDays += Carbon::parse($hireDate)->diffInDays(today());
|
|
$count++;
|
|
}
|
|
}
|
|
$avgTenureDays = $count > 0 ? (int) round($totalDays / $count) : 0;
|
|
}
|
|
|
|
return [
|
|
'total' => $total,
|
|
'active' => $active,
|
|
'resigned' => $resigned,
|
|
'hired_this_year' => $hiredThisYear,
|
|
'resigned_this_year' => $resignedThisYear,
|
|
'avg_tenure_days' => $avgTenureDays,
|
|
'avg_tenure_label' => $this->formatTenure($avgTenureDays),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* CSV 내보내기용 전체 데이터
|
|
*/
|
|
public function getTenureExportData(array $filters = []): Collection
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
$query = Employee::query()
|
|
->with(['user', 'department'])
|
|
->forTenant($tenantId)
|
|
->whereNotNull('json_extra->hire_date');
|
|
|
|
if (! empty($filters['department_id'])) {
|
|
$query->where('department_id', $filters['department_id']);
|
|
}
|
|
|
|
if (! empty($filters['status'])) {
|
|
if ($filters['status'] === 'active') {
|
|
$query->where('employee_status', 'active');
|
|
} elseif ($filters['status'] === 'resigned') {
|
|
$query->where('employee_status', 'resigned');
|
|
}
|
|
}
|
|
|
|
if (! empty($filters['hire_from'])) {
|
|
$query->where('json_extra->hire_date', '>=', $filters['hire_from']);
|
|
}
|
|
if (! empty($filters['hire_to'])) {
|
|
$query->where('json_extra->hire_date', '<=', $filters['hire_to']);
|
|
}
|
|
|
|
return $query->orderByRaw("JSON_UNQUOTE(JSON_EXTRACT(json_extra, '$.hire_date')) DESC")->get();
|
|
}
|
|
|
|
/**
|
|
* 근속일수 → "N년 M개월 D일" 변환
|
|
*/
|
|
public function formatTenure(int $days): string
|
|
{
|
|
if ($days <= 0) {
|
|
return '0일';
|
|
}
|
|
|
|
$years = intdiv($days, 365);
|
|
$remaining = $days % 365;
|
|
$months = intdiv($remaining, 30);
|
|
$d = $remaining % 30;
|
|
|
|
$parts = [];
|
|
if ($years > 0) {
|
|
$parts[] = "{$years}년";
|
|
}
|
|
if ($months > 0) {
|
|
$parts[] = "{$months}개월";
|
|
}
|
|
if ($d > 0 || empty($parts)) {
|
|
$parts[] = "{$d}일";
|
|
}
|
|
|
|
return implode(' ', $parts);
|
|
}
|
|
|
|
/**
|
|
* 직급/직책 추가
|
|
*/
|
|
public function createPosition(string $type, string $name): Position
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
|
|
// key 생성: 이름을 소문자+언더스코어로 변환, 한글은 그대로
|
|
$key = str_replace(' ', '_', mb_strtolower(trim($name)));
|
|
|
|
// 중복 체크 후 존재하면 기존 반환
|
|
$existing = Position::query()
|
|
->forTenant()
|
|
->where('type', $type)
|
|
->where('key', $key)
|
|
->first();
|
|
|
|
if ($existing) {
|
|
return $existing;
|
|
}
|
|
|
|
// 다음 sort_order
|
|
$maxSort = Position::query()
|
|
->forTenant()
|
|
->where('type', $type)
|
|
->max('sort_order') ?? 0;
|
|
|
|
return Position::create([
|
|
'tenant_id' => $tenantId,
|
|
'type' => $type,
|
|
'key' => $key,
|
|
'name' => trim($name),
|
|
'sort_order' => $maxSort + 1,
|
|
'is_active' => true,
|
|
]);
|
|
}
|
|
}
|