- 실시간 작업장 현황을 지도로 시각화 - 작업장 관리 페이지에서 정의한 구역 정보 활용 - TBM 작업자 및 방문자 현황 표시 주요 변경사항: - dashboard.html: 작업장 현황 섹션 추가 (기존 작업 현황 테이블 제거) - workplace-status.js: 지도 렌더링 및 데이터 통합 로직 구현 - modern-dashboard.js: 삭제된 DOM 요소 조건부 체크 추가 시각화 방식: - 인원 없음: 회색 테두리 + 작업장 이름 - 내부 작업자: 파란색 영역 + 인원 수 - 외부 방문자: 보라색 영역 + 인원 수 - 둘 다: 초록색 영역 + 총 인원 수 기술 구현: - Canvas API 기반 사각형 영역 렌더링 - map-regions API를 통한 데이터 일관성 보장 - 클릭 이벤트로 상세 정보 모달 표시 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
413 lines
11 KiB
JavaScript
413 lines
11 KiB
JavaScript
/**
|
|
* annual-vacation-overview.js
|
|
* 연간 연차 현황 페이지 로직 (2-탭 구조)
|
|
*/
|
|
|
|
import { API_BASE_URL } from './api-config.js';
|
|
|
|
// 전역 변수
|
|
let annualUsageChart = null;
|
|
let currentYear = new Date().getFullYear();
|
|
let vacationRequests = [];
|
|
|
|
/**
|
|
* 페이지 초기화
|
|
*/
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
// 관리자 권한 체크
|
|
const user = JSON.parse(localStorage.getItem('user') || '{}');
|
|
const isAdmin = user.role === 'Admin' || [1, 2].includes(user.role_id);
|
|
|
|
if (!isAdmin) {
|
|
alert('관리자만 접근할 수 있습니다');
|
|
window.location.href = '/pages/dashboard.html';
|
|
return;
|
|
}
|
|
|
|
initializeYearSelector();
|
|
initializeMonthSelector();
|
|
initializeEventListeners();
|
|
await loadAnnualUsageData();
|
|
});
|
|
|
|
/**
|
|
* 연도 선택 초기화
|
|
*/
|
|
function initializeYearSelector() {
|
|
const yearSelect = document.getElementById('yearSelect');
|
|
const currentYear = new Date().getFullYear();
|
|
|
|
// 최근 5년, 현재 연도, 다음 연도
|
|
for (let year = currentYear - 5; year <= currentYear + 1; year++) {
|
|
const option = document.createElement('option');
|
|
option.value = year;
|
|
option.textContent = `${year}년`;
|
|
if (year === currentYear) {
|
|
option.selected = true;
|
|
}
|
|
yearSelect.appendChild(option);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 월 선택 초기화
|
|
*/
|
|
function initializeMonthSelector() {
|
|
const monthSelect = document.getElementById('monthSelect');
|
|
const currentMonth = new Date().getMonth() + 1;
|
|
|
|
// 현재 월을 기본 선택
|
|
monthSelect.value = currentMonth;
|
|
}
|
|
|
|
/**
|
|
* 이벤트 리스너 초기화
|
|
*/
|
|
function initializeEventListeners() {
|
|
// 탭 전환
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
const tabName = e.target.dataset.tab;
|
|
switchTab(tabName);
|
|
});
|
|
});
|
|
|
|
// 조회 버튼
|
|
document.getElementById('refreshBtn').addEventListener('click', async () => {
|
|
await loadAnnualUsageData();
|
|
const activeTab = document.querySelector('.tab-btn.active').dataset.tab;
|
|
if (activeTab === 'monthlyDetails') {
|
|
await loadMonthlyDetails();
|
|
}
|
|
});
|
|
|
|
// 연도 변경 시 자동 조회
|
|
document.getElementById('yearSelect').addEventListener('change', async () => {
|
|
await loadAnnualUsageData();
|
|
const activeTab = document.querySelector('.tab-btn.active').dataset.tab;
|
|
if (activeTab === 'monthlyDetails') {
|
|
await loadMonthlyDetails();
|
|
}
|
|
});
|
|
|
|
// 월 선택 변경 시
|
|
document.getElementById('monthSelect').addEventListener('change', loadMonthlyDetails);
|
|
|
|
// 엑셀 다운로드
|
|
document.getElementById('exportExcelBtn').addEventListener('click', exportToExcel);
|
|
}
|
|
|
|
/**
|
|
* 탭 전환
|
|
*/
|
|
function switchTab(tabName) {
|
|
// 탭 버튼 활성화
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
if (btn.dataset.tab === tabName) {
|
|
btn.classList.add('active');
|
|
}
|
|
});
|
|
|
|
// 탭 콘텐츠 활성화
|
|
document.querySelectorAll('.tab-content').forEach(content => {
|
|
content.classList.remove('active');
|
|
});
|
|
|
|
if (tabName === 'annualUsage') {
|
|
document.getElementById('annualUsageTab').classList.add('active');
|
|
} else if (tabName === 'monthlyDetails') {
|
|
document.getElementById('monthlyDetailsTab').classList.add('active');
|
|
loadMonthlyDetails();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 연간 사용 데이터 로드 (탭 1)
|
|
*/
|
|
async function loadAnnualUsageData() {
|
|
const year = document.getElementById('yearSelect').value;
|
|
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
|
|
// 해당 연도의 모든 승인된 휴가 신청 조회
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/api/vacation-requests?start_date=${year}-01-01&end_date=${year}-12-31&status=approved`,
|
|
{
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('휴가 데이터를 불러오는데 실패했습니다');
|
|
}
|
|
|
|
const result = await response.json();
|
|
vacationRequests = result.data || [];
|
|
|
|
// 월별로 집계
|
|
const monthlyData = aggregateMonthlyUsage(vacationRequests);
|
|
|
|
// 잔여 일수 계산 (올해 총 부여 - 사용)
|
|
const remainingDays = await calculateRemainingDays(year);
|
|
|
|
updateAnnualUsageChart(monthlyData, remainingDays);
|
|
} catch (error) {
|
|
console.error('연간 사용 데이터 로드 오류:', error);
|
|
showToast('데이터를 불러오는데 실패했습니다', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 월별 사용 일수 집계
|
|
*/
|
|
function aggregateMonthlyUsage(requests) {
|
|
const monthlyUsage = Array(12).fill(0); // 1월~12월
|
|
|
|
requests.forEach(req => {
|
|
const startDate = new Date(req.start_date);
|
|
const endDate = new Date(req.end_date);
|
|
const daysUsed = req.days_used || 0;
|
|
|
|
// 간단한 집계: 시작일의 월에 모든 일수를 할당
|
|
// (더 정교한 계산이 필요하면 일자별로 쪼개야 함)
|
|
const month = startDate.getMonth(); // 0-11
|
|
monthlyUsage[month] += daysUsed;
|
|
});
|
|
|
|
return monthlyUsage;
|
|
}
|
|
|
|
/**
|
|
* 잔여 일수 계산
|
|
*/
|
|
async function calculateRemainingDays(year) {
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
|
|
// 전체 작업자의 휴가 잔액 조회
|
|
const response = await fetch(`${API_BASE_URL}/api/vacation-balances/year/${year}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
return 0;
|
|
}
|
|
|
|
const result = await response.json();
|
|
const balances = result.data || [];
|
|
|
|
// 전체 잔여 일수 합계
|
|
const totalRemaining = balances.reduce((sum, item) => sum + (item.remaining_days || 0), 0);
|
|
return totalRemaining;
|
|
} catch (error) {
|
|
console.error('잔여 일수 계산 오류:', error);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 연간 사용 차트 업데이트
|
|
*/
|
|
function updateAnnualUsageChart(monthlyData, remainingDays) {
|
|
const ctx = document.getElementById('annualUsageChart');
|
|
|
|
// 기존 차트 삭제
|
|
if (annualUsageChart) {
|
|
annualUsageChart.destroy();
|
|
}
|
|
|
|
const labels = ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월', '잔여'];
|
|
const data = [...monthlyData, remainingDays];
|
|
|
|
annualUsageChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: '일수',
|
|
data: data,
|
|
backgroundColor: data.map((_, idx) =>
|
|
idx === 12 ? 'rgba(16, 185, 129, 0.8)' : 'rgba(59, 130, 246, 0.8)'
|
|
),
|
|
borderColor: data.map((_, idx) =>
|
|
idx === 12 ? 'rgba(16, 185, 129, 1)' : 'rgba(59, 130, 246, 1)'
|
|
),
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return `${context.parsed.y}일`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
stepSize: 5
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 월별 상세 기록 로드 (탭 2)
|
|
*/
|
|
async function loadMonthlyDetails() {
|
|
const year = document.getElementById('yearSelect').value;
|
|
const month = document.getElementById('monthSelect').value;
|
|
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
|
|
// 해당 월의 모든 휴가 신청 조회 (승인된 것만)
|
|
const startDate = `${year}-${String(month).padStart(2, '0')}-01`;
|
|
const lastDay = new Date(year, month, 0).getDate();
|
|
const endDate = `${year}-${String(month).padStart(2, '0')}-${lastDay}`;
|
|
|
|
const response = await fetch(
|
|
`${API_BASE_URL}/api/vacation-requests?start_date=${startDate}&end_date=${endDate}&status=approved`,
|
|
{
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('월별 데이터를 불러오는데 실패했습니다');
|
|
}
|
|
|
|
const result = await response.json();
|
|
const monthlyRequests = result.data || [];
|
|
|
|
updateMonthlyTable(monthlyRequests);
|
|
} catch (error) {
|
|
console.error('월별 상세 기록 로드 오류:', error);
|
|
showToast('데이터를 불러오는데 실패했습니다', 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 월별 테이블 업데이트
|
|
*/
|
|
function updateMonthlyTable(requests) {
|
|
const tbody = document.getElementById('monthlyTableBody');
|
|
|
|
if (requests.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" class="loading-state">
|
|
<p>데이터가 없습니다</p>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = requests.map(req => {
|
|
const statusText = req.status === 'approved' ? '승인' : req.status === 'pending' ? '대기' : '거부';
|
|
const statusClass = req.status === 'approved' ? 'success' : req.status === 'pending' ? 'warning' : 'danger';
|
|
|
|
return `
|
|
<tr>
|
|
<td>${req.worker_name}</td>
|
|
<td>${req.vacation_type_name}</td>
|
|
<td>${req.start_date}</td>
|
|
<td>${req.end_date}</td>
|
|
<td>${req.days_used}일</td>
|
|
<td>${req.reason || '-'}</td>
|
|
<td><span class="badge badge-${statusClass}">${statusText}</span></td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
/**
|
|
* 엑셀 다운로드
|
|
*/
|
|
function exportToExcel() {
|
|
const year = document.getElementById('yearSelect').value;
|
|
const month = document.getElementById('monthSelect').value;
|
|
const tbody = document.getElementById('monthlyTableBody');
|
|
|
|
// 테이블에 데이터가 없으면 중단
|
|
if (!tbody.querySelector('tr:not(.loading-state)')) {
|
|
showToast('다운로드할 데이터가 없습니다', 'warning');
|
|
return;
|
|
}
|
|
|
|
// CSV 형식으로 데이터 생성
|
|
const headers = ['작업자명', '휴가유형', '시작일', '종료일', '사용일수', '사유', '상태'];
|
|
const rows = Array.from(tbody.querySelectorAll('tr:not(.loading-state)')).map(tr => {
|
|
const cells = tr.querySelectorAll('td');
|
|
return Array.from(cells).map(cell => {
|
|
// badge 클래스가 있으면 텍스트만 추출
|
|
const badge = cell.querySelector('.badge');
|
|
return badge ? badge.textContent : cell.textContent;
|
|
});
|
|
});
|
|
|
|
const csvContent = [
|
|
headers.join(','),
|
|
...rows.map(row => row.join(','))
|
|
].join('\n');
|
|
|
|
// BOM 추가 (한글 깨짐 방지)
|
|
const BOM = '\uFEFF';
|
|
const blob = new Blob([BOM + csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', `월별_연차_상세_${year}_${month}월.csv`);
|
|
link.style.visibility = 'hidden';
|
|
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
showToast('엑셀 파일이 다운로드되었습니다', 'success');
|
|
}
|
|
|
|
/**
|
|
* 토스트 메시지 표시
|
|
*/
|
|
function showToast(message, type = 'info') {
|
|
const container = document.getElementById('toastContainer');
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-${type}`;
|
|
toast.textContent = message;
|
|
|
|
container.appendChild(toast);
|
|
|
|
setTimeout(() => {
|
|
toast.classList.add('show');
|
|
}, 10);
|
|
|
|
setTimeout(() => {
|
|
toast.classList.remove('show');
|
|
setTimeout(() => {
|
|
container.removeChild(toast);
|
|
}, 300);
|
|
}, 3000);
|
|
}
|