feat(tksupport): Sprint 001 Section C — 전사 휴가관리 구현
- 전사 휴가 부여/관리 (company-holidays) CRUD + 연차차감 트랜잭션 - 전체 휴가관리 대시보드 (vacation-dashboard) 부서별/직원별 현황 - 내 휴가 현황 개선 (/my-status) balance_type별 카드, 전사 휴가일 - requireSupportTeam 미들웨어, 부서명 JOIN, 마이그레이션 002 추가 - 사이드바 roles 기반 메뉴 필터링 (하위호환 유지) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<title>휴가 승인 - TK 행정지원</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="/static/css/tksupport.css?v=2026031401">
|
||||
<link rel="stylesheet" href="/static/css/tksupport.css?v=2026032301">
|
||||
</head>
|
||||
<body>
|
||||
<header class="bg-purple-700 text-white sticky top-0 z-50">
|
||||
@@ -54,6 +54,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>신청자</th>
|
||||
<th class="hide-mobile">부서</th>
|
||||
<th>유형</th>
|
||||
<th>기간</th>
|
||||
<th class="text-center">일수</th>
|
||||
@@ -63,7 +64,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pendingBody">
|
||||
<tr><td colspan="7" class="text-center text-gray-400 py-8">로딩 중...</td></tr>
|
||||
<tr><td colspan="8" class="text-center text-gray-400 py-8">로딩 중...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -83,6 +84,12 @@
|
||||
<option value="cancelled">취소</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">부서</label>
|
||||
<select id="filterAllDept" class="input-field px-3 py-2 rounded-lg text-sm">
|
||||
<option value="">전체</option>
|
||||
</select>
|
||||
</div>
|
||||
<button onclick="loadAllRequests()" class="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700">
|
||||
<i class="fas fa-search mr-1"></i>조회
|
||||
</button>
|
||||
@@ -92,6 +99,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>신청자</th>
|
||||
<th class="hide-mobile">부서</th>
|
||||
<th>유형</th>
|
||||
<th>기간</th>
|
||||
<th class="text-center">일수</th>
|
||||
@@ -101,7 +109,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="allRequestsBody">
|
||||
<tr><td colspan="7" class="text-center text-gray-400 py-8">조회 버튼을 클릭하세요</td></tr>
|
||||
<tr><td colspan="8" class="text-center text-gray-400 py-8">조회 버튼을 클릭하세요</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -195,7 +203,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/tksupport-core.js?v=2026031401"></script>
|
||||
<script src="/static/js/tksupport-core.js?v=2026032301"></script>
|
||||
<script>
|
||||
let reviewAction = '';
|
||||
let reviewRequestId = null;
|
||||
@@ -260,12 +268,13 @@
|
||||
document.getElementById('pendingCount').textContent = data.length;
|
||||
const tbody = document.getElementById('pendingBody');
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="empty-state"><i class="fas fa-check-circle block text-green-400"></i>대기 중인 신청이 없습니다</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="empty-state"><i class="fas fa-check-circle block text-green-400"></i>대기 중인 신청이 없습니다</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = data.map(r => `
|
||||
<tr>
|
||||
<td class="font-medium">${escapeHtml(r.user_name || r.username)}</td>
|
||||
<td class="hide-mobile text-gray-500 text-sm">${escapeHtml(r.department_name || '-')}</td>
|
||||
<td>${escapeHtml(r.vacation_type_name)}</td>
|
||||
<td class="whitespace-nowrap">${formatDate(r.start_date)}${r.start_date !== r.end_date ? ' ~ ' + formatDate(r.end_date) : ''}</td>
|
||||
<td class="text-center">${r.days_used}</td>
|
||||
@@ -284,18 +293,33 @@
|
||||
|
||||
async function loadAllRequests() {
|
||||
const status = document.getElementById('filterAllStatus').value;
|
||||
const params = status ? `?status=${status}` : '';
|
||||
const deptId = document.getElementById('filterAllDept').value;
|
||||
let params = [];
|
||||
if (status) params.push('status=' + status);
|
||||
if (deptId) params.push('department_id=' + deptId);
|
||||
const qs = params.length > 0 ? '?' + params.join('&') : '';
|
||||
try {
|
||||
const res = await api('/vacation/requests' + params);
|
||||
const res = await api('/vacation/requests' + qs);
|
||||
const data = res.data;
|
||||
const tbody = document.getElementById('allRequestsBody');
|
||||
|
||||
// 부서 필터 옵션 갱신
|
||||
const deptSel = document.getElementById('filterAllDept');
|
||||
const currentDept = deptSel.value;
|
||||
const depts = [...new Set(data.map(r => r.department_name).filter(Boolean))].sort();
|
||||
deptSel.innerHTML = '<option value="">전체</option>';
|
||||
depts.forEach(d => {
|
||||
deptSel.innerHTML += `<option value="" ${d === currentDept ? 'selected' : ''}>${escapeHtml(d)}</option>`;
|
||||
});
|
||||
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">내역이 없습니다</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="empty-state">내역이 없습니다</td></tr>';
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = data.map(r => `
|
||||
<tr>
|
||||
<td class="font-medium">${escapeHtml(r.user_name || r.username)}</td>
|
||||
<td class="hide-mobile text-gray-500 text-sm">${escapeHtml(r.department_name || '-')}</td>
|
||||
<td>${escapeHtml(r.vacation_type_name)}</td>
|
||||
<td class="whitespace-nowrap">${formatDate(r.start_date)}${r.start_date !== r.end_date ? ' ~ ' + formatDate(r.end_date) : ''}</td>
|
||||
<td class="text-center">${r.days_used}</td>
|
||||
|
||||
Reference in New Issue
Block a user