sso_users.user_id를 단일 식별자로 통합. JWT에서 worker_id 제거, department_id/is_production 추가. 백엔드 15개 모델, 11개 컨트롤러, 4개 서비스, 7개 라우트, 프론트엔드 32+ JS/11+ HTML 변환. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
844 lines
25 KiB
JavaScript
844 lines
25 KiB
JavaScript
/**
|
|
* 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('sso_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('sso_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.user_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('sso_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 = `
|
|
<tr><td colspan="8" class="loading-state"><p>작업자를 선택하세요</p></td></tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const token = localStorage.getItem('sso_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 = `
|
|
<tr><td colspan="8" class="loading-state"><p>등록된 휴가가 없습니다</p></td></tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = currentWorkerBalances.map(balance => `
|
|
<tr>
|
|
<td>${balance.worker_name || '-'}</td>
|
|
<td>${balance.year}</td>
|
|
<td>${balance.type_name} ${balance.is_special ? '<span class="badge badge-info">특별</span>' : ''}</td>
|
|
<td>${balance.total_days}일</td>
|
|
<td>${balance.used_days}일</td>
|
|
<td>${balance.remaining_days}일</td>
|
|
<td>${balance.notes || '-'}</td>
|
|
<td class="action-buttons">
|
|
<button class="btn btn-sm btn-secondary btn-icon" onclick="window.editBalance(${balance.id})">✏️</button>
|
|
<button class="btn btn-sm btn-danger btn-icon" onclick="window.deleteBalance(${balance.id})">🗑️</button>
|
|
</td>
|
|
</tr>
|
|
`).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.user_id == workerId);
|
|
if (!worker || !worker.hire_date) {
|
|
showToast('작업자의 입사일 정보가 없습니다', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const token = localStorage.getItem('sso_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({
|
|
user_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 = `
|
|
<strong>자동 계산 완료</strong><br>
|
|
입사일: ${worker.hire_date}<br>
|
|
계산된 연차: ${result.data.calculated_days}일<br>
|
|
아래 "총 부여 일수"에 자동으로 입력됩니다.
|
|
`;
|
|
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('sso_token');
|
|
const response = await fetch(`${API_BASE_URL}/api/vacation-balances`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
user_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('sso_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('sso_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 {
|
|
user_id: worker.user_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 {
|
|
user_id: worker.user_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'
|
|
? '<span class="badge badge-error">오류</span>'
|
|
: '<span class="badge badge-success">준비</span>';
|
|
|
|
return `
|
|
<tr>
|
|
<td>${item.worker_name}</td>
|
|
<td>${item.hire_date}</td>
|
|
<td>${item.years_worked}년</td>
|
|
<td>${item.calculated_days}일</td>
|
|
<td>${item.reason}</td>
|
|
<td>${statusBadge}</td>
|
|
</tr>
|
|
`;
|
|
}).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('sso_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({
|
|
user_id: item.user_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 = `
|
|
<tr><td colspan="7" class="loading-state"><p>등록된 휴가 유형이 없습니다</p></td></tr>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = vacationTypes.map(type => `
|
|
<tr>
|
|
<td>${type.type_name}</td>
|
|
<td>${type.type_code}</td>
|
|
<td>${type.priority}</td>
|
|
<td>${type.is_special ? '<span class="badge badge-info">특별</span>' : '-'}</td>
|
|
<td>${type.is_system ? '<span class="badge badge-warning">시스템</span>' : '-'}</td>
|
|
<td>${type.description || '-'}</td>
|
|
<td class="action-buttons">
|
|
<button class="btn btn-sm btn-secondary btn-icon" onclick="window.editVacationType(${type.id})" ${type.is_system ? 'disabled' : ''}>✏️</button>
|
|
<button class="btn btn-sm btn-danger btn-icon" onclick="window.deleteVacationType(${type.id})" ${type.is_system ? 'disabled' : ''}>🗑️</button>
|
|
</td>
|
|
</tr>
|
|
`).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('sso_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('sso_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');
|
|
});
|
|
}
|
|
|
|
// showToast → api-base.js 전역 사용
|