feat: [leave] 잔여연차 퇴사자 포함 및 퇴사일 기준 연차 계산

- getBalanceSummary에 resigned 상태 포함
- 퇴사자 연차는 퇴사일까지만 산출
- 퇴사자 1년 미만 재계산 대상 제외
- 상태 컬럼 추가 (재직/휴직/퇴사 배지)
- 퇴사자 행 회색 배경 시각적 구분
- 근속 계산: 퇴사자는 입사일~퇴사일 기준
This commit is contained in:
김보곤
2026-02-27 13:16:08 +09:00
parent b0c22f6efd
commit c3bb357513
2 changed files with 43 additions and 9 deletions

View File

@@ -232,11 +232,11 @@ public function getBalanceSummary(?int $year = null, ?string $sort = null, ?stri
// (1) 테넌트 연차 정책 조회
$policy = LeavePolicy::forTenant($tenantId)->first();
// (2) 재직/휴직 직원 전체 조회
// (2) 재직/휴직/퇴사 직원 전체 조회
$employees = Employee::query()
->with(['user:id,name', 'department:id,name'])
->forTenant($tenantId)
->whereIn('employee_status', ['active', 'leave'])
->whereIn('employee_status', ['active', 'leave', 'resigned'])
->get();
// (3) 기존 balance 일괄 조회
@@ -277,6 +277,7 @@ public function getBalanceSummary(?int $year = null, ?string $sort = null, ?stri
$employeesByUserId = $employees->keyBy('user_id');
// (6) 현재연도 + 1년 미만 직원은 매번 재계산 (월별 발생 방식)
// 퇴사자는 퇴사일 기준으로 확정되므로 재계산 대상에서 제외
if ($year === (int) now()->year) {
foreach ($existingBalances as $balance) {
$employee = $employeesByUserId->get($balance->user_id);
@@ -284,6 +285,10 @@ public function getBalanceSummary(?int $year = null, ?string $sort = null, ?stri
continue;
}
if ($employee->employee_status === 'resigned') {
continue;
}
$hire = Carbon::parse($employee->hire_date);
if ($hire->diffInMonths(today()) < 12) {
$recalculated = $this->calculateAnnualLeaveDays($employee, $year, $policy);
@@ -311,6 +316,10 @@ public function getBalanceSummary(?int $year = null, ?string $sort = null, ?stri
'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',
'status' => fn ($b) => match ($b->employee?->employee_status) {
'active' => 1, 'leave' => 2, 'resigned' => 3, default => 9,
},
'resign_date' => fn ($b) => $b->employee?->resign_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,
@@ -345,6 +354,15 @@ private function calculateAnnualLeaveDays(Employee $employee, int $year, ?LeaveP
? today()
: Carbon::create($year, 12, 31);
// 퇴사자: 기준일을 퇴사일로 제한
$resignDate = $employee->resign_date;
if ($resignDate) {
$resign = Carbon::parse($resignDate);
if ($resign->lessThan($referenceDate)) {
$referenceDate = $resign;
}
}
// 아직 입사 전이면 0일
if ($hire->greaterThan($referenceDate)) {
return 0;

View File

@@ -5,8 +5,9 @@
$sortColumns = [
'name' => ['label' => '사원', 'align' => 'left'],
'department' => ['label' => '부서', 'align' => 'left'],
'status' => ['label' => '상태', 'align' => 'center'],
'hire_date' => ['label' => '입사일', 'align' => 'center'],
'resign_date' => ['label' => '퇴사일', 'align' => 'center', 'sortable' => false],
'resign_date' => ['label' => '퇴사일', 'align' => 'center'],
'tenure' => ['label' => '근속', 'align' => 'center', 'sortable' => false],
'total_days' => ['label' => '부여', 'align' => 'center'],
'used_days' => ['label' => '사용', 'align' => 'center'],
@@ -60,25 +61,29 @@ class="inline-flex items-center gap-1 hover:text-blue-600 transition-colors">
$displayName = $employee?->display_name ?? $balance->user?->name ?? '-';
$department = $employee?->department;
$hireDate = $employee?->hire_date;
$isResigned = $employee?->employee_status === 'resigned';
$isLeave = $employee?->employee_status === 'leave';
$tenureParts = null;
if ($hireDate) {
$hire = \Carbon\Carbon::parse($hireDate);
$tenureYears = (int) $hire->diffInYears(now());
$tenureMonths = (int) $hire->copy()->addYears($tenureYears)->diffInMonths(now());
// 퇴사자: 입사일~퇴사일, 재직자: 입사일~오늘
$tenureEnd = ($isResigned && $employee?->resign_date) ? \Carbon\Carbon::parse($employee->resign_date) : now();
$tenureYears = (int) $hire->diffInYears($tenureEnd);
$tenureMonths = (int) $hire->copy()->addYears($tenureYears)->diffInMonths($tenureEnd);
$tenureParts = ['years' => $tenureYears, 'months' => $tenureMonths];
}
$remaining = $balance->total_days - $balance->used_days;
$rate = $balance->total_days > 0 ? round(($balance->used_days / $balance->total_days) * 100) : 0;
$barColor = $rate >= 90 ? 'bg-red-500' : ($rate >= 70 ? 'bg-amber-500' : 'bg-blue-500');
@endphp
<tr class="hover:bg-gray-50 transition-colors">
<tr class="{{ $isResigned ? 'bg-gray-100 opacity-75' : 'hover:bg-gray-50' }} transition-colors">
{{-- 사원 --}}
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center gap-2">
<div class="shrink-0 w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-xs font-medium">
<div class="shrink-0 w-8 h-8 rounded-full {{ $isResigned ? 'bg-gray-200 text-gray-500' : 'bg-blue-100 text-blue-600' }} flex items-center justify-center text-xs font-medium">
{{ mb_substr($displayName, 0, 1) }}
</div>
<span class="text-sm font-medium text-gray-900">{{ $displayName }}</span>
<span class="text-sm font-medium {{ $isResigned ? 'text-gray-500' : 'text-gray-900' }}">{{ $displayName }}</span>
</div>
</td>
@@ -87,6 +92,17 @@ class="inline-flex items-center gap-1 hover:text-blue-600 transition-colors">
{{ $department?->name ?? '-' }}
</td>
{{-- 상태 --}}
<td class="px-6 py-4 whitespace-nowrap text-center">
@if($isResigned)
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-200 text-gray-600">퇴사</span>
@elseif($isLeave)
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-700">휴직</span>
@else
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-700">재직</span>
@endif
</td>
{{-- 입사일 --}}
<td class="px-6 py-4 whitespace-nowrap text-center text-sm text-gray-700">
{{ $hireDate ?? '-' }}
@@ -143,7 +159,7 @@ class="inline-flex items-center gap-1 hover:text-blue-600 transition-colors">
</tr>
@empty
<tr>
<td colspan="9" class="px-6 py-12 text-center">
<td colspan="10" class="px-6 py-12 text-center">
<div class="flex flex-col items-center gap-2">
<svg class="w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>