/** * vacation-allocation.js * 휴가 발생 입력 페이지 로직 */ import { API_BASE_URL } from './api-config.js'; // 전역 변수 let workers = []; let vacationTypes = []; let currentWorkerBalances = []; /** * 페이지 초기화 */ document.addEventListener('DOMContentLoaded', async () => { // 관리자 권한 체크 const user = JSON.parse(localStorage.getItem('user') || '{}'); console.log('Current user:', user); console.log('Role ID:', user.role_id, 'Role:', user.role); // role이 'Admin'이거나 role_id가 1 또는 2인 경우 허용 const isAdmin = user.role === 'Admin' || [1, 2].includes(user.role_id); if (!isAdmin) { console.error('Access denied. User:', user); alert('관리자만 접근할 수 있습니다'); window.location.href = '/pages/dashboard.html'; return; } await loadInitialData(); initializeYearSelectors(); initializeTabNavigation(); initializeEventListeners(); }); /** * 초기 데이터 로드 */ async function loadInitialData() { await Promise.all([ loadWorkers(), loadVacationTypes() ]); } /** * 작업자 목록 로드 */ async function loadWorkers() { try { const token = localStorage.getItem('token'); console.log('Loading workers... Token:', token ? 'exists' : 'missing'); const response = await fetch(`${API_BASE_URL}/api/workers`, { headers: { 'Authorization': `Bearer ${token}` } }); console.log('Workers API Response status:', response.status); if (!response.ok) { const errorData = await response.json(); console.error('Workers API Error:', errorData); throw new Error(errorData.message || '작업자 목록 로드 실패'); } const result = await response.json(); console.log('Workers data:', result); workers = result.data || []; if (workers.length === 0) { console.warn('No workers found in database'); showToast('등록된 작업자가 없습니다', 'warning'); return; } // 개별 입력 탭 - 작업자 셀렉트 박스 const selectWorker = document.getElementById('individualWorker'); workers.forEach(worker => { const option = document.createElement('option'); option.value = worker.worker_id; option.textContent = `${worker.worker_name} (${worker.employment_status === 'employed' ? '재직' : '퇴사'})`; selectWorker.appendChild(option); }); console.log(`Loaded ${workers.length} workers successfully`); } catch (error) { console.error('작업자 로드 오류:', error); showToast(`작업자 목록을 불러오는데 실패했습니다: ${error.message}`, 'error'); } } /** * 휴가 유형 목록 로드 */ async function loadVacationTypes() { try { const token = localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/api/vacation-types`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) throw new Error('휴가 유형 로드 실패'); const result = await response.json(); vacationTypes = result.data || []; // 개별 입력 탭 - 휴가 유형 셀렉트 박스 const selectType = document.getElementById('individualVacationType'); vacationTypes.forEach(type => { const option = document.createElement('option'); option.value = type.id; option.textContent = `${type.type_name} ${type.is_special ? '(특별)' : ''}`; selectType.appendChild(option); }); // 특별 휴가 관리 탭 테이블 로드 loadSpecialTypesTable(); } catch (error) { console.error('휴가 유형 로드 오류:', error); showToast('휴가 유형을 불러오는데 실패했습니다', 'error'); } } /** * 연도 셀렉터 초기화 */ function initializeYearSelectors() { const currentYear = new Date().getFullYear(); const yearSelectors = ['individualYear', 'bulkYear']; yearSelectors.forEach(selectorId => { const select = document.getElementById(selectorId); for (let year = currentYear - 1; year <= currentYear + 2; year++) { const option = document.createElement('option'); option.value = year; option.textContent = `${year}년`; if (year === currentYear) { option.selected = true; } select.appendChild(option); } }); } /** * 탭 네비게이션 초기화 */ function initializeTabNavigation() { const tabButtons = document.querySelectorAll('.tab-button'); tabButtons.forEach(button => { button.addEventListener('click', () => { const tabName = button.dataset.tab; switchTab(tabName); }); }); } /** * 탭 전환 */ function switchTab(tabName) { // 탭 버튼 활성화 document.querySelectorAll('.tab-button').forEach(btn => { btn.classList.remove('active'); }); document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); // 탭 콘텐츠 표시 document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(`tab-${tabName}`).classList.add('active'); } /** * 이벤트 리스너 초기화 */ function initializeEventListeners() { // === 탭 1: 개별 입력 === document.getElementById('individualWorker').addEventListener('change', loadWorkerBalances); document.getElementById('autoCalculateBtn').addEventListener('click', autoCalculateAnnualLeave); document.getElementById('individualSubmitBtn').addEventListener('click', submitIndividualVacation); document.getElementById('individualResetBtn').addEventListener('click', resetIndividualForm); // === 탭 2: 일괄 입력 === document.getElementById('bulkPreviewBtn').addEventListener('click', previewBulkAllocation); document.getElementById('bulkSubmitBtn').addEventListener('click', submitBulkAllocation); // === 탭 3: 특별 휴가 관리 === document.getElementById('addSpecialTypeBtn').addEventListener('click', () => openVacationTypeModal()); // 모달 닫기 document.querySelectorAll('.modal-close').forEach(btn => { btn.addEventListener('click', closeModals); }); // 모달 폼 제출 document.getElementById('vacationTypeForm').addEventListener('submit', submitVacationType); document.getElementById('editBalanceForm').addEventListener('submit', submitEditBalance); } // ============================================================================= // 탭 1: 개별 입력 // ============================================================================= /** * 작업자의 기존 휴가 잔액 로드 */ async function loadWorkerBalances() { const workerId = document.getElementById('individualWorker').value; const year = document.getElementById('individualYear').value; if (!workerId) { document.getElementById('individualTableBody').innerHTML = `

작업자를 선택하세요

`; return; } try { const token = localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/api/vacation-balances/worker/${workerId}/year/${year}`, { headers: { 'Authorization': `Bearer ${token}` } }); if (!response.ok) throw new Error('휴가 잔액 로드 실패'); const result = await response.json(); currentWorkerBalances = result.data || []; updateWorkerBalancesTable(); } catch (error) { console.error('휴가 잔액 로드 오류:', error); showToast('휴가 잔액을 불러오는데 실패했습니다', 'error'); } } /** * 작업자 휴가 잔액 테이블 업데이트 */ function updateWorkerBalancesTable() { const tbody = document.getElementById('individualTableBody'); if (currentWorkerBalances.length === 0) { tbody.innerHTML = `

등록된 휴가가 없습니다

`; return; } tbody.innerHTML = currentWorkerBalances.map(balance => ` ${balance.worker_name || '-'} ${balance.year} ${balance.type_name} ${balance.is_special ? '특별' : ''} ${balance.total_days}일 ${balance.used_days}일 ${balance.remaining_days}일 ${balance.notes || '-'} `).join(''); } /** * 자동 계산 (연차만 해당) */ async function autoCalculateAnnualLeave() { const workerId = document.getElementById('individualWorker').value; const year = document.getElementById('individualYear').value; const typeId = document.getElementById('individualVacationType').value; if (!workerId) { showToast('작업자를 선택하세요', 'warning'); return; } // 선택한 휴가 유형이 ANNUAL인지 확인 const selectedType = vacationTypes.find(t => t.id == typeId); if (!selectedType || selectedType.type_code !== 'ANNUAL') { showToast('연차(ANNUAL) 유형만 자동 계산이 가능합니다', 'warning'); return; } // 작업자의 입사일 조회 const worker = workers.find(w => w.worker_id == workerId); if (!worker || !worker.hire_date) { showToast('작업자의 입사일 정보가 없습니다', 'error'); return; } try { const token = localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/api/vacation-balances/auto-calculate`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ worker_id: workerId, hire_date: worker.hire_date, year: year }) }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '자동 계산 실패'); } // 계산 결과 표시 const resultDiv = document.getElementById('autoCalculateResult'); resultDiv.innerHTML = ` 자동 계산 완료
입사일: ${worker.hire_date}
계산된 연차: ${result.data.calculated_days}일
아래 "총 부여 일수"에 자동으로 입력됩니다. `; resultDiv.style.display = 'block'; // 폼에 자동 입력 document.getElementById('individualTotalDays').value = result.data.calculated_days; document.getElementById('individualNotes').value = `근속년수 기반 자동 계산 (입사일: ${worker.hire_date})`; showToast(result.message, 'success'); // 기존 데이터 새로고침 await loadWorkerBalances(); } catch (error) { console.error('자동 계산 오류:', error); showToast(error.message, 'error'); } } /** * 개별 휴가 제출 */ async function submitIndividualVacation() { const workerId = document.getElementById('individualWorker').value; const year = document.getElementById('individualYear').value; const typeId = document.getElementById('individualVacationType').value; const totalDays = document.getElementById('individualTotalDays').value; const usedDays = document.getElementById('individualUsedDays').value || 0; const notes = document.getElementById('individualNotes').value; if (!workerId || !year || !typeId || !totalDays) { showToast('필수 항목을 모두 입력하세요', 'warning'); return; } try { const token = localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/api/vacation-balances`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ worker_id: workerId, vacation_type_id: typeId, year: year, total_days: parseFloat(totalDays), used_days: parseFloat(usedDays), notes: notes }) }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '저장 실패'); } showToast('휴가가 등록되었습니다', 'success'); resetIndividualForm(); await loadWorkerBalances(); } catch (error) { console.error('휴가 등록 오류:', error); showToast(error.message, 'error'); } } /** * 개별 입력 폼 초기화 */ function resetIndividualForm() { document.getElementById('individualVacationType').value = ''; document.getElementById('individualTotalDays').value = ''; document.getElementById('individualUsedDays').value = '0'; document.getElementById('individualNotes').value = ''; document.getElementById('autoCalculateResult').style.display = 'none'; } /** * 휴가 수정 (전역 함수로 노출) */ window.editBalance = function(balanceId) { const balance = currentWorkerBalances.find(b => b.id === balanceId); if (!balance) return; document.getElementById('editBalanceId').value = balance.id; document.getElementById('editTotalDays').value = balance.total_days; document.getElementById('editUsedDays').value = balance.used_days; document.getElementById('editNotes').value = balance.notes || ''; document.getElementById('editBalanceModal').classList.add('active'); }; /** * 휴가 삭제 (전역 함수로 노출) */ window.deleteBalance = async function(balanceId) { if (!confirm('정말 삭제하시겠습니까?')) return; try { const token = localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/api/vacation-balances/${balanceId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '삭제 실패'); } showToast('삭제되었습니다', 'success'); await loadWorkerBalances(); } catch (error) { console.error('삭제 오류:', error); showToast(error.message, 'error'); } }; /** * 휴가 수정 제출 */ async function submitEditBalance(e) { e.preventDefault(); const balanceId = document.getElementById('editBalanceId').value; const totalDays = document.getElementById('editTotalDays').value; const usedDays = document.getElementById('editUsedDays').value; const notes = document.getElementById('editNotes').value; try { const token = localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/api/vacation-balances/${balanceId}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ total_days: parseFloat(totalDays), used_days: parseFloat(usedDays), notes: notes }) }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '수정 실패'); } showToast('수정되었습니다', 'success'); closeModals(); await loadWorkerBalances(); } catch (error) { console.error('수정 오류:', error); showToast(error.message, 'error'); } } // ============================================================================= // 탭 2: 일괄 입력 // ============================================================================= let bulkPreviewData = []; /** * 일괄 할당 미리보기 */ async function previewBulkAllocation() { const year = document.getElementById('bulkYear').value; const employmentStatus = document.getElementById('bulkEmploymentStatus').value; // 필터링된 작업자 목록 let targetWorkers = workers; if (employmentStatus === 'employed') { targetWorkers = workers.filter(w => w.employment_status === 'employed'); } // ANNUAL 유형 찾기 const annualType = vacationTypes.find(t => t.type_code === 'ANNUAL'); if (!annualType) { showToast('ANNUAL 휴가 유형이 없습니다', 'error'); return; } // 미리보기 데이터 생성 bulkPreviewData = targetWorkers.map(worker => { const hireDate = worker.hire_date; if (!hireDate) { return { worker_id: worker.worker_id, worker_name: worker.worker_name, hire_date: '-', years_worked: '-', calculated_days: 0, reason: '입사일 정보 없음', status: 'error' }; } const calculatedDays = calculateAnnualLeaveDays(hireDate, year); const yearsWorked = calculateYearsWorked(hireDate, year); return { worker_id: worker.worker_id, worker_name: worker.worker_name, hire_date: hireDate, years_worked: yearsWorked, calculated_days: calculatedDays, reason: getCalculationReason(yearsWorked, calculatedDays), status: 'ready' }; }); updateBulkPreviewTable(); document.getElementById('bulkPreviewSection').style.display = 'block'; document.getElementById('bulkSubmitBtn').disabled = false; } /** * 연차 일수 계산 (한국 근로기준법) */ function calculateAnnualLeaveDays(hireDate, targetYear) { const hire = new Date(hireDate); const targetDate = new Date(targetYear, 0, 1); const monthsDiff = (targetDate.getFullYear() - hire.getFullYear()) * 12 + (targetDate.getMonth() - hire.getMonth()); // 1년 미만: 월 1일 if (monthsDiff < 12) { return Math.floor(monthsDiff); } // 1년 이상: 15일 기본 + 2년마다 1일 추가 (최대 25일) const yearsWorked = Math.floor(monthsDiff / 12); const additionalDays = Math.floor((yearsWorked - 1) / 2); return Math.min(15 + additionalDays, 25); } /** * 근속년수 계산 */ function calculateYearsWorked(hireDate, targetYear) { const hire = new Date(hireDate); const targetDate = new Date(targetYear, 0, 1); const monthsDiff = (targetDate.getFullYear() - hire.getFullYear()) * 12 + (targetDate.getMonth() - hire.getMonth()); return (monthsDiff / 12).toFixed(1); } /** * 계산 근거 생성 */ function getCalculationReason(yearsWorked, days) { const years = parseFloat(yearsWorked); if (years < 1) { return `입사 ${Math.floor(years * 12)}개월 (월 1일)`; } if (days === 25) { return '최대 25일 (근속 3년 이상)'; } return `근속 ${Math.floor(years)}년 (15일 + ${days - 15}일)`; } /** * 일괄 미리보기 테이블 업데이트 */ function updateBulkPreviewTable() { const tbody = document.getElementById('bulkPreviewTableBody'); tbody.innerHTML = bulkPreviewData.map(item => { const statusBadge = item.status === 'error' ? '오류' : '준비'; return ` ${item.worker_name} ${item.hire_date} ${item.years_worked}년 ${item.calculated_days}일 ${item.reason} ${statusBadge} `; }).join(''); } /** * 일괄 할당 제출 */ async function submitBulkAllocation() { const year = document.getElementById('bulkYear').value; // 오류가 없는 항목만 필터링 const validItems = bulkPreviewData.filter(item => item.status !== 'error' && item.calculated_days > 0); if (validItems.length === 0) { showToast('생성할 항목이 없습니다', 'warning'); return; } if (!confirm(`${validItems.length}명의 연차를 생성하시겠습니까?`)) { return; } // ANNUAL 유형 찾기 const annualType = vacationTypes.find(t => t.type_code === 'ANNUAL'); let successCount = 0; let failCount = 0; for (const item of validItems) { try { const token = localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/api/vacation-balances/auto-calculate`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ worker_id: item.worker_id, hire_date: item.hire_date, year: year }) }); if (response.ok) { successCount++; } else { failCount++; } } catch (error) { failCount++; } } showToast(`완료: ${successCount}건 성공, ${failCount}건 실패`, successCount > 0 ? 'success' : 'error'); // 미리보기 초기화 document.getElementById('bulkPreviewSection').style.display = 'none'; document.getElementById('bulkSubmitBtn').disabled = true; bulkPreviewData = []; } // ============================================================================= // 탭 3: 특별 휴가 관리 // ============================================================================= /** * 특별 휴가 유형 테이블 로드 */ function loadSpecialTypesTable() { const tbody = document.getElementById('specialTypesTableBody'); if (vacationTypes.length === 0) { tbody.innerHTML = `

등록된 휴가 유형이 없습니다

`; return; } tbody.innerHTML = vacationTypes.map(type => ` ${type.type_name} ${type.type_code} ${type.priority} ${type.is_special ? '특별' : '-'} ${type.is_system ? '시스템' : '-'} ${type.description || '-'} `).join(''); } /** * 휴가 유형 모달 열기 */ function openVacationTypeModal(typeId = null) { const modal = document.getElementById('vacationTypeModal'); const form = document.getElementById('vacationTypeForm'); form.reset(); if (typeId) { const type = vacationTypes.find(t => t.id === typeId); if (!type) return; document.getElementById('modalTitle').textContent = '휴가 유형 수정'; document.getElementById('modalTypeId').value = type.id; document.getElementById('modalTypeName').value = type.type_name; document.getElementById('modalTypeCode').value = type.type_code; document.getElementById('modalPriority').value = type.priority; document.getElementById('modalIsSpecial').checked = type.is_special === 1; document.getElementById('modalDescription').value = type.description || ''; } else { document.getElementById('modalTitle').textContent = '휴가 유형 추가'; document.getElementById('modalTypeId').value = ''; } modal.classList.add('active'); } /** * 휴가 유형 수정 (전역 함수) */ window.editVacationType = function(typeId) { openVacationTypeModal(typeId); }; /** * 휴가 유형 삭제 (전역 함수) */ window.deleteVacationType = async function(typeId) { if (!confirm('정말 삭제하시겠습니까?')) return; try { const token = localStorage.getItem('token'); const response = await fetch(`${API_BASE_URL}/api/vacation-types/${typeId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '삭제 실패'); } showToast('삭제되었습니다', 'success'); await loadVacationTypes(); } catch (error) { console.error('삭제 오류:', error); showToast(error.message, 'error'); } }; /** * 휴가 유형 제출 */ async function submitVacationType(e) { e.preventDefault(); const typeId = document.getElementById('modalTypeId').value; const typeName = document.getElementById('modalTypeName').value; const typeCode = document.getElementById('modalTypeCode').value; const priority = document.getElementById('modalPriority').value; const isSpecial = document.getElementById('modalIsSpecial').checked ? 1 : 0; const description = document.getElementById('modalDescription').value; const data = { type_name: typeName, type_code: typeCode.toUpperCase(), priority: parseInt(priority), is_special: isSpecial, description: description }; try { const token = localStorage.getItem('token'); const url = typeId ? `${API_BASE_URL}/api/vacation-types/${typeId}` : `${API_BASE_URL}/api/vacation-types`; const response = await fetch(url, { method: typeId ? 'PUT' : 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (!response.ok) { throw new Error(result.message || '저장 실패'); } showToast(typeId ? '수정되었습니다' : '추가되었습니다', 'success'); closeModals(); await loadVacationTypes(); } catch (error) { console.error('저장 오류:', error); showToast(error.message, 'error'); } } // ============================================================================= // 공통 함수 // ============================================================================= /** * 모달 닫기 */ function closeModals() { document.querySelectorAll('.modal').forEach(modal => { modal.classList.remove('active'); }); } /** * 토스트 메시지 */ 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); }