feat: [hr] 사원관리 영업팀 제외 및 강제 제외 기능 추가
- 영업팀 포함 부서 사원 기본 제외 (외부직원) - json_extra.is_excluded 플래그로 강제 제외/복원 토글 - 필터에 '제외 사원 표시' 체크박스 추가 - 제외 사원 시각적 구분 (주황 배경, 제외 뱃지)
This commit is contained in:
@@ -300,6 +300,38 @@ public function forceDestroy(Request $request, int $id): JsonResponse|Response
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 제외/복원 토글
|
||||
*/
|
||||
public function toggleExclude(Request $request, int $id): JsonResponse|Response
|
||||
{
|
||||
$employee = $this->employeeService->toggleExclude($id);
|
||||
|
||||
if (! $employee) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '사원 정보를 찾을 수 없습니다.',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$isExcluded = $employee->getJsonExtraValue('is_excluded', false);
|
||||
|
||||
if ($request->header('HX-Request')) {
|
||||
$employees = $this->employeeService->getEmployees(
|
||||
$request->all(),
|
||||
$request->integer('per_page', 20)
|
||||
);
|
||||
|
||||
return response(view('hr.employees.partials.table', compact('employees')));
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => $isExcluded ? '사원이 목록에서 제외되었습니다.' : '사원이 목록에 복원되었습니다.',
|
||||
'is_excluded' => $isExcluded,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 사원 첨부파일 업로드 (로컬 + GCS 듀얼 저장)
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,8 @@ public function __construct(
|
||||
*/
|
||||
public function index(): View
|
||||
{
|
||||
$stats = $this->employeeService->getStats();
|
||||
$showExcluded = request()->boolean('show_excluded');
|
||||
$stats = $this->employeeService->getStats($showExcluded);
|
||||
$departments = $this->employeeService->getDepartments();
|
||||
|
||||
return view('hr.employees.index', [
|
||||
|
||||
@@ -26,6 +26,21 @@ public function getEmployees(array $filters = [], int $perPage = 20): LengthAwar
|
||||
->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'];
|
||||
@@ -89,12 +104,23 @@ public function getEmployeeById(int $id): ?Employee
|
||||
/**
|
||||
* 사원 통계
|
||||
*/
|
||||
public function getStats(): array
|
||||
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(),
|
||||
@@ -394,6 +420,23 @@ public function forceDeleteEmployee(int $id): array
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 부서 목록 (드롭다운용)
|
||||
*/
|
||||
|
||||
@@ -86,7 +86,13 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 f
|
||||
<option value="default" {{ request('sort_by') === 'default' ? 'selected' : '' }}>상태순</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<div class="shrink-0 flex items-end gap-3">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer py-2">
|
||||
<input type="checkbox" name="show_excluded" value="1"
|
||||
{{ request('show_excluded') ? 'checked' : '' }}
|
||||
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
||||
<span class="text-xs text-gray-500 whitespace-nowrap">제외 사원 표시</span>
|
||||
</label>
|
||||
<button type="submit"
|
||||
hx-get="{{ route('api.admin.hr.employees.index') }}"
|
||||
hx-target="#employees-table"
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
@forelse($employees as $employee)
|
||||
<tr class="hover:bg-gray-50 transition-colors {{ $employee->employee_status === 'resigned' ? 'opacity-50' : '' }}">
|
||||
@php $isExcludedRow = $employee->getJsonExtraValue('is_excluded', false) || ($employee->department && str_contains($employee->department->name, '영업팀')); @endphp
|
||||
<tr class="hover:bg-gray-50 transition-colors {{ $employee->employee_status === 'resigned' ? 'opacity-50' : '' }} {{ $isExcludedRow ? 'bg-orange-50 opacity-60' : '' }}">
|
||||
{{-- 사원 정보 --}}
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<a href="{{ route('hr.employees.show', $employee->id) }}"
|
||||
@@ -49,8 +50,11 @@ class="flex items-center gap-3 group">
|
||||
{{ mb_substr($employee->display_name ?? $employee->user?->name ?? '?', 0, 1) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-900 group-hover:text-blue-600">
|
||||
<div class="text-sm font-medium text-gray-900 group-hover:text-blue-600 flex items-center gap-1.5">
|
||||
{{ $employee->display_name ?? $employee->user?->name ?? '-' }}
|
||||
@if($isExcludedRow)
|
||||
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-orange-100 text-orange-600">제외</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@@ -129,6 +133,28 @@ class="text-blue-600 hover:text-blue-800" title="수정">
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
{{-- 제외/복원 --}}
|
||||
@php $isExcluded = $employee->getJsonExtraValue('is_excluded', false); @endphp
|
||||
<button type="button"
|
||||
hx-post="{{ route('api.admin.hr.employees.toggle-exclude', $employee->id) }}"
|
||||
hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'
|
||||
hx-target="#employees-table"
|
||||
hx-swap="innerHTML"
|
||||
hx-include="#employeeFilterForm"
|
||||
hx-confirm="{{ $isExcluded ? ($employee->display_name ?? $employee->user?->name) . '님을 사원 목록에 복원하시겠습니까?' : ($employee->display_name ?? $employee->user?->name) . '님을 사원 목록에서 제외하시겠습니까?' }}"
|
||||
class="{{ $isExcluded ? 'text-green-600 hover:text-green-800' : 'text-orange-500 hover:text-orange-700' }}"
|
||||
title="{{ $isExcluded ? '복원' : '제외' }}">
|
||||
@if($isExcluded)
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
@else
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/>
|
||||
</svg>
|
||||
@endif
|
||||
</button>
|
||||
|
||||
{{-- 퇴직 처리 --}}
|
||||
@if($employee->employee_status !== 'resigned')
|
||||
<button type="button"
|
||||
|
||||
@@ -1148,6 +1148,7 @@
|
||||
Route::put('/{id}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'update'])->name('update');
|
||||
Route::delete('/{id}', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'destroy'])->name('destroy');
|
||||
Route::delete('/{id}/force', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'forceDestroy'])->name('force-destroy');
|
||||
Route::post('/{id}/toggle-exclude', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'toggleExclude'])->name('toggle-exclude');
|
||||
|
||||
// 첨부파일
|
||||
Route::post('/{id}/files', [\App\Http\Controllers\Api\Admin\HR\EmployeeController::class, 'uploadFile'])->name('upload-file');
|
||||
|
||||
Reference in New Issue
Block a user