feat: [leave] 잔여연차 테이블 헤더 클릭 정렬 기능 추가
- 사원, 부서, 입사일, 부여, 사용, 잔여, 소진율 컬럼 정렬 지원 - 기본 정렬: 입사일 오름차순 (빠른 순) - 활성 정렬 컬럼 파란색 강조 + 방향 화살표 표시
This commit is contained in:
@@ -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([
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user