feat: [leave] 잔여연차 테이블 헤더 클릭 정렬 기능 추가

- 사원, 부서, 입사일, 부여, 사용, 잔여, 소진율 컬럼 정렬 지원
- 기본 정렬: 입사일 오름차순 (빠른 순)
- 활성 정렬 컬럼 파란색 강조 + 방향 화살표 표시
This commit is contained in:
김보곤
2026-02-27 13:06:42 +09:00
parent 3d295e1ca7
commit d99fdcc2ec
4 changed files with 77 additions and 17 deletions

View File

@@ -181,10 +181,12 @@ public function cancel(Request $request, int $id): JsonResponse
public function balance(Request $request): JsonResponse|Response
{
$year = $request->integer('year', now()->year);
$balances = $this->leaveService->getBalanceSummary($year);
$sort = $request->input('sort', 'hire_date');
$direction = $request->input('direction', 'asc');
$balances = $this->leaveService->getBalanceSummary($year, $sort, $direction);
if ($request->header('HX-Request')) {
return response(view('hr.leaves.partials.balance', compact('balances', 'year')));
return response(view('hr.leaves.partials.balance', compact('balances', 'year', 'sort', 'direction')));
}
return response()->json([

View File

@@ -224,7 +224,7 @@ public function cancel(int $id): ?Leave
* 사원관리의 모든 재직/휴직 직원을 표시하며,
* balance 레코드가 없는 직원은 자동 생성한다.
*/
public function getBalanceSummary(?int $year = null): Collection
public function getBalanceSummary(?int $year = null, ?string $sort = null, ?string $direction = null): Collection
{
$tenantId = session('selected_tenant_id');
$year = $year ?? now()->year;
@@ -294,16 +294,31 @@ public function getBalanceSummary(?int $year = null): Collection
}
}
return $existingBalances
$result = $existingBalances
->filter(fn ($balance) => $employeesByUserId->has($balance->user_id))
->map(function ($balance) use ($employeesByUserId) {
$employee = $employeesByUserId->get($balance->user_id);
$balance->employee = $employee;
return $balance;
})
->sortBy(fn ($balance) => $balance->employee?->display_name ?? '')
->values();
});
// 정렬 (기본: 입사일 오름차순)
$sortField = $sort ?? 'hire_date';
$isDesc = ($direction ?? 'asc') === 'desc';
$sortCallback = match ($sortField) {
'name' => fn ($b) => $b->employee?->display_name ?? '',
'department' => fn ($b) => $b->employee?->department?->name ?? '',
'hire_date' => fn ($b) => $b->employee?->hire_date ?? '9999-12-31',
'total_days' => fn ($b) => $b->total_days,
'used_days' => fn ($b) => $b->used_days,
'remaining' => fn ($b) => $b->total_days - $b->used_days,
'rate' => fn ($b) => $b->total_days > 0 ? $b->used_days / $b->total_days : 0,
default => fn ($b) => $b->employee?->hire_date ?? '9999-12-31',
};
return ($isDesc ? $result->sortByDesc($sortCallback) : $result->sortBy($sortCallback))->values();
}
/**

View File

@@ -311,12 +311,17 @@ function switchTab(tab) {
}
}
function loadBalance() {
let balanceSort = 'hire_date';
let balanceDirection = 'asc';
function loadBalance(sort, direction) {
if (sort) balanceSort = sort;
if (direction) balanceDirection = direction;
const year = document.getElementById('balanceYear').value;
htmx.ajax('GET', '{{ route("api.admin.hr.leaves.balance") }}', {
target: '#balance-container',
swap: 'innerHTML',
values: { year: year },
values: { year: year, sort: balanceSort, direction: balanceDirection },
});
}

View File

@@ -1,17 +1,55 @@
{{-- 잔여연차 현황 (HTMX로 로드) --}}
@php
$currentSort = $sort ?? 'hire_date';
$currentDir = $direction ?? 'asc';
$sortColumns = [
'name' => ['label' => '사원', 'align' => 'left'],
'department' => ['label' => '부서', 'align' => 'left'],
'hire_date' => ['label' => '입사일', 'align' => 'center'],
'tenure' => ['label' => '근속', 'align' => 'center', 'sortable' => false],
'total_days' => ['label' => '부여', 'align' => 'center'],
'used_days' => ['label' => '사용', 'align' => 'center'],
'remaining' => ['label' => '잔여', 'align' => 'center'],
'rate' => ['label' => '소진율', 'align' => 'center'],
];
@endphp
<x-table-swipe>
<table class="min-w-full">
<thead class="bg-gray-50 border-b border-gray-200">
<tr>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-600">사원</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-600">부서</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-600">입사일</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-600">근속</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-600">부여</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-600">사용</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-600">잔여</th>
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-600">소진율</th>
@foreach($sortColumns as $col => $meta)
@php
$sortable = $meta['sortable'] ?? true;
$isActive = $sortable && $currentSort === $col;
$nextDir = $isActive && $currentDir === 'asc' ? 'desc' : 'asc';
$align = $meta['align'] === 'left' ? 'text-left' : 'text-center';
@endphp
<th class="px-6 py-3 {{ $align }} text-sm font-semibold {{ $isActive ? 'text-blue-700' : 'text-gray-600' }}">
@if($sortable)
<button type="button"
onclick="loadBalance('{{ $col }}', '{{ $nextDir }}')"
class="inline-flex items-center gap-1 hover:text-blue-600 transition-colors">
{{ $meta['label'] }}
@if($isActive)
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@if($currentDir === 'asc')
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7"/>
@else
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
@endif
</svg>
@else
<svg class="w-3.5 h-3.5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
</svg>
@endif
</button>
@else
{{ $meta['label'] }}
@endif
</th>
@endforeach
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">