feat: [leave] 휴가 신청 시 결재선 선택 기능 추가

- 휴가 신청 모달에 결재선 드롭다운 + 미리보기 UI 추가
- 선택된 결재선으로 결재 생성 (미선택 시 기본결재선 fallback)
- 휴가 목록에 결재진행 컬럼 추가 (원형 아이콘: ✓승인/✗반려/숫자대기/파랑현재)
- approval.steps.approver eager load 추가
This commit is contained in:
김보곤
2026-03-03 22:36:05 +09:00
parent 81b64f25aa
commit 511bfa3ec5
5 changed files with 114 additions and 13 deletions

View File

@@ -182,7 +182,7 @@ class="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:
<div id="leaveModal" class="fixed inset-0 z-50 hidden">
<div class="fixed inset-0 bg-black/40" onclick="closeLeaveModal()"></div>
<div class="fixed inset-0 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl w-full max-w-md relative">
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg relative">
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-semibold text-gray-800">휴가 신청</h3>
<button type="button" onclick="closeLeaveModal()" class="text-gray-400 hover:text-gray-600">
@@ -231,6 +231,22 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 f
<span id="balanceDisplay" class="font-semibold text-blue-800">-</span>
</div>
</div>
{{-- 결재선 선택 --}}
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">결재선 <span class="text-red-500">*</span></label>
<select name="approval_line_id" id="leaveApprovalLine" required
onchange="previewApprovalSteps()"
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
@foreach($approvalLines as $line)
<option value="{{ $line->id }}" {{ $line->is_default ? 'selected' : '' }}
data-steps='@json($line->steps)'>
{{ $line->name }}{{ $line->is_default ? ' (기본)' : '' }} {{ count($line->steps ?? []) }}단계
</option>
@endforeach
</select>
<div id="approvalStepsPreview" class="mt-2 flex items-center gap-1 overflow-x-auto py-1"></div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">사유</label>
<textarea name="reason" rows="3" maxlength="1000" placeholder="휴가 사유를 입력하세요..."
@@ -364,11 +380,46 @@ function exportLeaves() {
window.location.href = '{{ route("api.admin.hr.leaves.export") }}?' + params.toString();
}
// ===== 결재선 미리보기 =====
function previewApprovalSteps() {
const select = document.getElementById('leaveApprovalLine');
const preview = document.getElementById('approvalStepsPreview');
if (!select || !preview) return;
const option = select.options[select.selectedIndex];
if (!option || !option.dataset.steps) {
preview.innerHTML = '';
return;
}
let steps;
try { steps = JSON.parse(option.dataset.steps); } catch { steps = []; }
if (!steps.length) {
preview.innerHTML = '<span class="text-xs text-gray-400">결재 단계가 없습니다.</span>';
return;
}
const typeLabels = { approval: '결재', agreement: '합의', reference: '참조' };
const typeColors = { approval: '#2563EB', agreement: '#D97706', reference: '#6B7280' };
const typeBgs = { approval: '#EFF6FF', agreement: '#FFFBEB', reference: '#F9FAFB' };
preview.innerHTML = steps.map((s, i) => {
const type = s.step_type || s.type || 'approval';
const color = typeColors[type] || '#6B7280';
const bg = typeBgs[type] || '#F9FAFB';
const name = s.user_name || '사용자 ' + s.user_id;
const arrow = i > 0 ? '<span style="color:#D1D5DB;margin:0 2px;">→</span>' : '';
return arrow + '<span style="display:inline-flex;align-items:center;gap:2px;padding:2px 8px;border-radius:9999px;font-size:12px;white-space:nowrap;background:' + bg + ';color:' + color + ';">'
+ name + ' <span style="color:' + color + ';opacity:0.7;">(' + (typeLabels[type] || type) + ')</span></span>';
}).join('');
}
// ===== 휴가 신청 모달 =====
function openLeaveModal() {
document.getElementById('leaveForm').reset();
document.getElementById('balanceInfo').classList.add('hidden');
document.getElementById('leaveModal').classList.remove('hidden');
previewApprovalSteps();
}
function closeLeaveModal() {

View File

@@ -14,6 +14,7 @@
<th class="px-6 py-3 text-center 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-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>
</tr>
@@ -79,6 +80,39 @@
</span>
</td>
{{-- 결재진행 --}}
<td class="px-4 py-4 whitespace-nowrap">
@if($leave->approval && $leave->approval->steps->count() > 0)
<div class="flex items-center gap-0.5">
@foreach($leave->approval->steps->sortBy('step_order') as $step)
@php
$stepColor = match($step->status) {
'approved' => 'bg-green-500',
'rejected' => 'bg-red-500',
'on_hold' => 'bg-amber-500',
default => 'bg-gray-300',
};
$isCurrent = $step->status === 'pending'
&& $step->step_order == $leave->approval->current_step
&& in_array($step->step_type, ['approval', 'agreement']);
if ($isCurrent) $stepColor = 'bg-blue-500 ring-2 ring-blue-200';
@endphp
<div class="relative group">
<div class="w-6 h-6 rounded-full {{ $stepColor }} flex items-center justify-center text-white text-xs font-medium"
title="{{ $step->approver?->name ?? '미지정' }} ({{ match($step->step_type) { 'approval' => '결재', 'agreement' => '합의', 'reference' => '참조', default => '' } }})">
@if($step->status === 'approved')
@elseif($step->status === 'rejected')
@else {{ $step->step_order }}
@endif
</div>
</div>
@endforeach
</div>
@else
<span class="text-xs text-gray-400">-</span>
@endif
</td>
{{-- 처리자 --}}
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
@if($leave->approver)
@@ -122,7 +156,7 @@ class="px-2.5 py-1 text-xs font-medium text-red-700 bg-red-50 hover:bg-red-100 r
</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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>