Files
tk-factory-services/user-management/web/static/js/tkuser-vacation-settings.js
Hyungi Ahn a3f7a324b1 feat(tkuser): 연차/휴가 관리 프론트엔드 개편 (Sprint 001 Section B)
- workers 기반 → sso_users 기반 전환 (vacWorkers→vacUsers, /workers→/users)
- 휴가 탭: 부서 필터, 이름 검색, balance_type 뱃지, 장기근속 제외 체크박스
- 배정 모달: balance_type/expires_at 필드 추가, 사용자 부서별 optgroup
- 부서 탭: 팀장 표시/편집, 승인권한 CRUD
- 연차 설정 탭/JS 신규: 기본연차·장기근속·이월연차 설정 UI
- API 미완성 대응: 필드명 폴백(worker_id→user_id), 404 graceful degradation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:14:28 +09:00

159 lines
8.9 KiB
JavaScript

/* ===== Vacation Settings ===== */
let vacSettingsLoaded = false, vacSettingsData = {};
async function loadVacationSettingsTab() {
if (vacSettingsLoaded) return;
await loadVacationSettings();
vacSettingsLoaded = true;
}
async function loadVacationSettings() {
const c = document.getElementById('vacSettingsContent');
try {
const r = await api('/vacation-settings');
const arr = r.data || r;
vacSettingsData = {};
(Array.isArray(arr) ? arr : []).forEach(s => { vacSettingsData[s.setting_key] = s; });
renderVacationSettings();
} catch (e) {
c.innerHTML = `<div class="bg-white rounded-xl shadow-sm p-8 text-center">
<i class="fas fa-exclamation-circle text-3xl text-gray-300 mb-3"></i>
<p class="text-gray-500 text-sm">설정을 불러올 수 없습니다</p>
<p class="text-gray-400 text-xs mt-1">${escHtml(e.message)}</p>
</div>`;
}
}
function _vsVal(key, fallback) {
const s = vacSettingsData[key];
return s ? s.setting_value : fallback;
}
function _vsDesc(key) {
const s = vacSettingsData[key];
return s && s.description ? s.description : '';
}
function renderVacationSettings() {
const c = document.getElementById('vacSettingsContent');
const isAdmin = currentUser && (currentUser.role === 'admin' || currentUser.role === 'system');
const dis = isAdmin ? '' : 'disabled';
c.innerHTML = `
<div class="space-y-6">
<!-- 기본 연차 -->
<div class="bg-white rounded-xl shadow-sm p-5">
<h3 class="text-base font-semibold text-gray-800 mb-4"><i class="fas fa-calendar-days text-blue-400 mr-2"></i>기본 연차 설정</h3>
<div class="grid sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">1년 미만 규칙</label>
<select id="vs_first_year_rule" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" ${dis}>
<option value="monthly" ${_vsVal('first_year_rule','monthly')==='monthly'?'selected':''}>월별 발생</option>
<option value="proportional" ${_vsVal('first_year_rule','monthly')==='proportional'?'selected':''}>비례 배분</option>
<option value="full" ${_vsVal('first_year_rule','monthly')==='full'?'selected':''}>전체 부여</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">기본 일수</label>
<input type="number" id="vs_base_days" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="${_vsVal('base_days','15')}" min="0" step="1" ${dis}>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">근속 가산 (N년당 1일)</label>
<input type="number" id="vs_increment_per_years" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="${_vsVal('increment_per_years','2')}" min="1" step="1" ${dis}>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">최대 일수</label>
<input type="number" id="vs_max_days" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="${_vsVal('max_days','25')}" min="0" step="1" ${dis}>
</div>
</div>
</div>
<!-- 장기근속 -->
<div class="bg-white rounded-xl shadow-sm p-5">
<h3 class="text-base font-semibold text-gray-800 mb-4"><i class="fas fa-award text-emerald-400 mr-2"></i>장기근속 보너스</h3>
<div class="grid sm:grid-cols-2 gap-4">
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">기준 근속년수</label>
<input type="number" id="vs_long_service_threshold_years" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="${_vsVal('long_service_threshold_years','5')}" min="1" step="1" ${dis}>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">보너스 일수</label>
<input type="number" id="vs_long_service_bonus_days" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="${_vsVal('long_service_bonus_days','5')}" min="0" step="1" ${dis}>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">만료 정책</label>
<select id="vs_long_service_expiry" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" ${dis}>
<option value="year_end" ${_vsVal('long_service_expiry','year_end')==='year_end'?'selected':''}>연말 만료</option>
<option value="never" ${_vsVal('long_service_expiry','year_end')==='never'?'selected':''}>만료 없음</option>
<option value="anniversary" ${_vsVal('long_service_expiry','year_end')==='anniversary'?'selected':''}>입사일 기준</option>
</select>
</div>
<div class="flex items-center">
<label class="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
<input type="checkbox" id="vs_long_service_auto_grant" class="rounded" ${_vsVal('long_service_auto_grant','true')==='true'?'checked':''} ${dis}>
자동 부여
</label>
</div>
</div>
</div>
<!-- 이월 연차 -->
<div class="bg-white rounded-xl shadow-sm p-5">
<h3 class="text-base font-semibold text-gray-800 mb-4"><i class="fas fa-rotate text-orange-400 mr-2"></i>이월 연차</h3>
<div class="grid sm:grid-cols-2 gap-4">
<div class="flex items-center">
<label class="flex items-center gap-2 text-sm text-gray-600 cursor-pointer">
<input type="checkbox" id="vs_carry_over_enabled" class="rounded" ${_vsVal('carry_over_enabled','false')==='true'?'checked':''} ${dis}>
이월 허용
</label>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">최대 이월 일수</label>
<input type="number" id="vs_carry_over_max_days" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="${_vsVal('carry_over_max_days','5')}" min="0" step="1" ${dis}>
</div>
<div>
<label class="block text-xs font-medium text-gray-600 mb-1">이월 만료월 (M월말)</label>
<input type="number" id="vs_carry_over_expiry_month" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="${_vsVal('carry_over_expiry_month','3')}" min="1" max="12" step="1" ${dis}>
</div>
</div>
</div>
${isAdmin ? `
<div class="flex justify-end">
<button onclick="saveVacationSettings()" class="px-6 py-2.5 bg-slate-700 text-white rounded-lg hover:bg-slate-800 text-sm font-medium">
<i class="fas fa-save mr-1.5"></i>설정 저장
</button>
</div>` : ''}
</div>`;
}
async function saveVacationSettings() {
const fields = {
first_year_rule: document.getElementById('vs_first_year_rule').value,
base_days: document.getElementById('vs_base_days').value,
increment_per_years: document.getElementById('vs_increment_per_years').value,
max_days: document.getElementById('vs_max_days').value,
long_service_threshold_years: document.getElementById('vs_long_service_threshold_years').value,
long_service_bonus_days: document.getElementById('vs_long_service_bonus_days').value,
long_service_expiry: document.getElementById('vs_long_service_expiry').value,
long_service_auto_grant: document.getElementById('vs_long_service_auto_grant').checked ? 'true' : 'false',
carry_over_enabled: document.getElementById('vs_carry_over_enabled').checked ? 'true' : 'false',
carry_over_max_days: document.getElementById('vs_carry_over_max_days').value,
carry_over_expiry_month: document.getElementById('vs_carry_over_expiry_month').value,
};
// Only send changed values
const changes = {};
for (const [key, val] of Object.entries(fields)) {
if (_vsVal(key, '') !== val) changes[key] = val;
}
if (!Object.keys(changes).length) { showToast('변경된 설정이 없습니다.', 'error'); return; }
try {
await api('/vacation-settings', { method: 'PUT', body: JSON.stringify({ settings: changes }) });
showToast('설정이 저장되었습니다.');
vacSettingsLoaded = false;
await loadVacationSettingsTab();
} catch (e) { showToast(e.message, 'error'); }
}