tkfb-core.js v=2026040104, tksupport/tksafety/tkpurchase/tkuser-core.js, system2 api-base.js, system3 app.js 캐시 버스팅 일괄 갱신. 브라우저 캐시에 남은 구버전(tkds 리다이렉트) 강제 갱신. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
241 lines
12 KiB
HTML
241 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<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=2026032301">
|
|
</head>
|
|
<body>
|
|
<!-- Header -->
|
|
<header class="bg-purple-700 text-white sticky top-0 z-50">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between items-center h-14">
|
|
<div class="flex items-center gap-3">
|
|
<button id="mobileMenuBtn" onclick="toggleMobileMenu()" class="lg:hidden text-purple-200 hover:text-white">
|
|
<i class="fas fa-bars text-xl"></i>
|
|
</button>
|
|
<i class="fas fa-building text-xl text-purple-200"></i>
|
|
<h1 class="text-lg font-semibold">TK 행정지원</h1>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<div id="headerUserName" class="text-sm font-medium hidden sm:block">-</div>
|
|
<div id="headerUserAvatar" class="w-8 h-8 bg-purple-600 rounded-full flex items-center justify-center text-sm font-semibold">-</div>
|
|
<button onclick="doLogout()" class="text-purple-200 hover:text-white" title="로그아웃"><i class="fas fa-sign-out-alt"></i></button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 fade-in">
|
|
<div class="flex gap-6">
|
|
<!-- Sidebar Nav -->
|
|
<nav id="sideNav" class="hidden lg:flex flex-col gap-1 w-48 flex-shrink-0 pt-2"></nav>
|
|
|
|
<!-- Main -->
|
|
<div class="flex-1 min-w-0">
|
|
<!-- 통계 카드 -->
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-5">
|
|
<div class="stat-card">
|
|
<div class="stat-value text-purple-600" id="statRemaining">-</div>
|
|
<div class="stat-label">잔여 연차</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value text-blue-600" id="statUsed">-</div>
|
|
<div class="stat-label">사용 연차</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value text-amber-600" id="statPending">-</div>
|
|
<div class="stat-label">대기 중</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value text-green-600" id="statApproved">-</div>
|
|
<div class="stat-label">승인 완료</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 빠른 신청 -->
|
|
<div class="bg-white rounded-xl shadow-sm p-5 mb-5">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-base font-semibold text-gray-800"><i class="fas fa-calendar-plus text-purple-500 mr-2"></i>빠른 휴가 신청</h2>
|
|
<a href="/vacation-request.html" class="text-sm text-purple-600 hover:text-purple-800">상세 신청 <i class="fas fa-arrow-right ml-1"></i></a>
|
|
</div>
|
|
<div id="hireDateWarning" class="hidden bg-amber-50 border border-amber-200 rounded-lg p-3 mb-4 text-sm text-amber-700">
|
|
<i class="fas fa-exclamation-triangle mr-1"></i>입사일이 미등록되어 연차가 자동 계산되지 않습니다. 관리자에게 수동 배정을 요청하세요.
|
|
</div>
|
|
<form id="quickRequestForm">
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">휴가 유형 <span class="text-red-400">*</span></label>
|
|
<select id="vacationType" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
|
|
<option value="">선택</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">시작일 <span class="text-red-400">*</span></label>
|
|
<input type="date" id="startDate" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">종료일 <span class="text-red-400">*</span></label>
|
|
<input type="date" id="endDate" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
|
|
</div>
|
|
<div>
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">사용일수 <span class="text-red-400">*</span></label>
|
|
<input type="number" id="daysUsed" step="0.5" min="0.5" class="input-field w-full px-3 py-2 rounded-lg text-sm" required>
|
|
</div>
|
|
</div>
|
|
<div class="mt-3">
|
|
<label class="block text-xs font-medium text-gray-600 mb-1">사유</label>
|
|
<input type="text" id="reason" class="input-field w-full px-3 py-2 rounded-lg text-sm" placeholder="사유를 입력하세요 (선택)">
|
|
</div>
|
|
<div class="flex justify-end mt-4">
|
|
<button type="submit" class="px-6 py-2.5 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium">
|
|
<i class="fas fa-paper-plane mr-2"></i>신청
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- 최근 신청 현황 -->
|
|
<div class="bg-white rounded-xl shadow-sm p-5">
|
|
<h2 class="text-base font-semibold text-gray-800 mb-4"><i class="fas fa-history text-purple-500 mr-2"></i>최근 신청 현황</h2>
|
|
<div class="overflow-x-auto">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>유형</th>
|
|
<th>기간</th>
|
|
<th class="text-center">일수</th>
|
|
<th>사유</th>
|
|
<th>상태</th>
|
|
<th class="text-right hide-mobile">신청일</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="recentRequestsBody">
|
|
<tr><td colspan="6" class="text-center text-gray-400 py-8">로딩 중...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="/static/js/tksupport-core.js?v=2026040101"></script>
|
|
<script>
|
|
let vacationTypes = [];
|
|
|
|
async function initDashboard() {
|
|
if (!initAuth()) return;
|
|
|
|
try {
|
|
// 휴가 유형, 잔여일, 최근 신청 병렬 로드
|
|
const [typesRes, balanceRes, requestsRes] = await Promise.all([
|
|
api('/vacation/types'),
|
|
api('/vacation/balance'),
|
|
api('/vacation/requests')
|
|
]);
|
|
|
|
vacationTypes = typesRes.data;
|
|
const sel = document.getElementById('vacationType');
|
|
vacationTypes.forEach(t => {
|
|
sel.innerHTML += `<option value="${t.id}">${escapeHtml(t.type_name)}</option>`;
|
|
});
|
|
|
|
// 잔여일 통계
|
|
const balances = balanceRes.data.balances;
|
|
const hireDate = balanceRes.data.hire_date;
|
|
let totalRemaining = 0, totalUsed = 0;
|
|
balances.forEach(b => {
|
|
totalRemaining += parseFloat(b.remaining_days || 0);
|
|
totalUsed += parseFloat(b.used_days || 0);
|
|
});
|
|
document.getElementById('statRemaining').textContent = totalRemaining % 1 === 0 ? totalRemaining : totalRemaining.toFixed(1);
|
|
document.getElementById('statUsed').textContent = totalUsed % 1 === 0 ? totalUsed : totalUsed.toFixed(1);
|
|
|
|
if (!hireDate) {
|
|
document.getElementById('hireDateWarning').classList.remove('hidden');
|
|
}
|
|
|
|
// 신청 현황 통계
|
|
const requests = requestsRes.data;
|
|
const pendingCount = requests.filter(r => r.status === 'pending').length;
|
|
const approvedCount = requests.filter(r => r.status === 'approved').length;
|
|
document.getElementById('statPending').textContent = pendingCount;
|
|
document.getElementById('statApproved').textContent = approvedCount;
|
|
|
|
// 최근 5건 표시
|
|
renderRecentRequests(requests.slice(0, 5));
|
|
} catch (err) {
|
|
console.error('Dashboard init error:', err);
|
|
showToast(err.message, 'error');
|
|
}
|
|
|
|
// 날짜 기본값
|
|
const today = new Date().toISOString().substring(0, 10);
|
|
document.getElementById('startDate').value = today;
|
|
document.getElementById('endDate').value = today;
|
|
document.getElementById('daysUsed').value = '1';
|
|
|
|
// 날짜 변경 시 자동 일수 계산
|
|
document.getElementById('startDate').addEventListener('change', calcDays);
|
|
document.getElementById('endDate').addEventListener('change', calcDays);
|
|
|
|
// 빠른 신청 폼
|
|
document.getElementById('quickRequestForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const vacation_type_id = document.getElementById('vacationType').value;
|
|
const start_date = document.getElementById('startDate').value;
|
|
const end_date = document.getElementById('endDate').value;
|
|
const days_used = parseFloat(document.getElementById('daysUsed').value);
|
|
const reason = document.getElementById('reason').value;
|
|
|
|
if (!vacation_type_id) { showToast('휴가 유형을 선택하세요', 'error'); return; }
|
|
|
|
try {
|
|
await api('/vacation/requests', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ vacation_type_id: parseInt(vacation_type_id), start_date, end_date, days_used, reason })
|
|
});
|
|
showToast('휴가 신청이 완료되었습니다');
|
|
setTimeout(() => location.reload(), 1000);
|
|
} catch (err) {
|
|
showToast(err.message, 'error');
|
|
}
|
|
});
|
|
}
|
|
|
|
function calcDays() {
|
|
const s = document.getElementById('startDate').value;
|
|
const e = document.getElementById('endDate').value;
|
|
if (s && e) {
|
|
const diff = Math.floor((new Date(e) - new Date(s)) / 86400000) + 1;
|
|
if (diff > 0) document.getElementById('daysUsed').value = diff;
|
|
}
|
|
}
|
|
|
|
function renderRecentRequests(requests) {
|
|
const tbody = document.getElementById('recentRequestsBody');
|
|
if (requests.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="empty-state"><i class="fas fa-calendar-times block"></i>신청 내역이 없습니다</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = requests.map(r => `
|
|
<tr>
|
|
<td>${escapeHtml(r.vacation_type_name)}</td>
|
|
<td>${formatDate(r.start_date)}${r.start_date !== r.end_date ? ' ~ ' + formatDate(r.end_date) : ''}</td>
|
|
<td class="text-center">${r.days_used}</td>
|
|
<td class="hide-mobile">${escapeHtml(r.reason || '-')}</td>
|
|
<td>${statusBadge(r.status)}</td>
|
|
<td class="text-right hide-mobile text-gray-500 text-xs">${formatDate(r.created_at)}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
initDashboard();
|
|
</script>
|
|
</body>
|
|
</html>
|