@@ -2239,17 +2318,18 @@
-
+
-
+
-
+
-
+
-
+
+
diff --git a/user-management/web/static/js/tkuser-core.js b/user-management/web/static/js/tkuser-core.js
index 5aaadf5..a9a7272 100644
--- a/user-management/web/static/js/tkuser-core.js
+++ b/user-management/web/static/js/tkuser-core.js
@@ -193,7 +193,7 @@ async function init() {
document.querySelectorAll('.fade-in').forEach(el => el.classList.add('visible'));
// URL ?tab= 파라미터로 탭 자동 전환 (화이트리스트 + URL 정리)
const ALLOWED_TABS = ['users','projects','workplaces','workers','departments',
- 'permissions','issueTypes','tasks','vacations','partners','vendors',
+ 'permissions','issueTypes','tasks','vacations','vacationSettings','partners','vendors',
'consumables','notificationRecipients','equipments'];
const urlTab = new URLSearchParams(location.search).get('tab');
if (urlTab && ALLOWED_TABS.includes(urlTab)) {
diff --git a/user-management/web/static/js/tkuser-departments.js b/user-management/web/static/js/tkuser-departments.js
index fb4b6e4..ca2bfff 100644
--- a/user-management/web/static/js/tkuser-departments.js
+++ b/user-management/web/static/js/tkuser-departments.js
@@ -1,6 +1,8 @@
/* ===== Departments CRUD ===== */
let departments = [], departmentsLoaded = false;
+const APPROVAL_TYPE_LABELS = { VACATION: '휴가', PURCHASE: '구매', DOCUMENT: '문서' };
+
async function loadDepartments() {
try {
const r = await api('/departments'); departments = r.data || r;
@@ -22,6 +24,7 @@ function displayDepartments() {
${d.department_name}
순서: ${d.display_order || 0}
+ | 팀장: ${d.leader_name ? escHtml(d.leader_name) : '미지정'}
@@ -57,23 +60,24 @@ async function showDeptMembers(deptId) {
if (!members.length) {
list.innerHTML = '
소속 인원이 없습니다
';
- return;
+ } else {
+ list.innerHTML = members.map(u => `
+
+
+
${(u.name || u.username).charAt(0)}
+
+
${u.name || u.username}
+
${u.username}
+
+
+
+ ${u.role === 'admin' ? '관리자' : '사용자'}
+ ${u.is_active === 0 || u.is_active === false ? '비활성' : '활성'}
+
+
`).join('');
}
- list.innerHTML = members.map(u => `
-
-
-
${(u.name || u.username).charAt(0)}
-
-
${u.name || u.username}
-
${u.username}
-
-
-
- ${u.role === 'admin' ? '관리자' : '사용자'}
- ${u.is_active === 0 || u.is_active === false ? '비활성' : '활성'}
-
-
`).join('');
+ loadApprovalAuthorities(deptId);
}
document.getElementById('addDepartmentForm').addEventListener('submit', async e => {
@@ -88,12 +92,29 @@ document.getElementById('addDepartmentForm').addEventListener('submit', async e
} catch(e) { showToast(e.message, 'error'); }
});
-function editDepartment(id) {
+async function editDepartment(id) {
const d = departments.find(x => x.department_id === id); if (!d) return;
document.getElementById('editDeptId').value = d.department_id;
document.getElementById('editDeptName').value = d.department_name;
document.getElementById('editDeptDescription').value = d.description || '';
document.getElementById('editDeptOrder').value = d.display_order || 0;
+
+ // Populate leader dropdown
+ const leaderSel = document.getElementById('editDeptLeader');
+ leaderSel.innerHTML = '
';
+ let deptUsers = users;
+ if (!deptUsers || !deptUsers.length) {
+ try { const r = await api('/users'); deptUsers = r.data || r; } catch(e) { /* ignore */ }
+ }
+ const members = (deptUsers || []).filter(u => u.department_id === d.department_id && u.is_active !== 0 && u.is_active !== false);
+ members.forEach(u => {
+ const o = document.createElement('option');
+ o.value = u.id;
+ o.textContent = u.name || u.username;
+ if (d.leader_user_id && d.leader_user_id === u.id) o.selected = true;
+ leaderSel.appendChild(o);
+ });
+
document.getElementById('editDepartmentModal').classList.remove('hidden');
}
function closeDepartmentModal() { document.getElementById('editDepartmentModal').classList.add('hidden'); }
@@ -101,10 +122,12 @@ function closeDepartmentModal() { document.getElementById('editDepartmentModal')
document.getElementById('editDepartmentForm').addEventListener('submit', async e => {
e.preventDefault();
try {
+ const leaderId = document.getElementById('editDeptLeader').value;
await api(`/departments/${document.getElementById('editDeptId').value}`, { method: 'PUT', body: JSON.stringify({
department_name: document.getElementById('editDeptName').value.trim(),
description: document.getElementById('editDeptDescription').value.trim() || null,
- display_order: parseInt(document.getElementById('editDeptOrder').value) || 0
+ display_order: parseInt(document.getElementById('editDeptOrder').value) || 0,
+ leader_user_id: leaderId ? parseInt(leaderId) : null
})});
showToast('수정되었습니다.'); closeDepartmentModal(); await loadDepartments();
await loadDepartmentsForSelect();
@@ -115,3 +138,80 @@ async function deleteDepartment(id, name) {
if (!confirm(`"${name}" 부서를 삭제하시겠습니까? 소속 인원은 부서 미지정으로 변경됩니다.`)) return;
try { await api(`/departments/${id}`, { method: 'DELETE' }); showToast('부서가 삭제되었습니다'); await loadDepartments(); } catch(e) { showToast(e.message, 'error'); }
}
+
+/* ===== Approval Authorities ===== */
+async function loadApprovalAuthorities(deptId) {
+ const section = document.getElementById('deptApprovalSection');
+ if (!section) return;
+ try {
+ const r = await api(`/departments/${deptId}/approval-authorities`);
+ const data = r.data || r;
+ section.classList.remove('hidden');
+ renderApprovalAuthorities(deptId, data);
+ } catch (e) {
+ section.classList.add('hidden');
+ }
+}
+
+function renderApprovalAuthorities(deptId, data) {
+ const list = document.getElementById('deptApprovalList');
+ if (!data || !data.length) {
+ list.innerHTML = '
등록된 승인권한이 없습니다
';
+ return;
+ }
+ list.innerHTML = `
+ | 유형 |
+ 승인자 |
+ 순서 |
+ |
+
${data.map(a => `
+ | ${APPROVAL_TYPE_LABELS[a.approval_type] || a.approval_type} |
+ ${escHtml(a.approver_name || '')} |
+ ${a.approval_order || 1} |
+ |
+
`).join('')}
`;
+}
+
+function openApprovalModal() {
+ if (!selectedDeptForMembers) return;
+ document.getElementById('approvalDeptId').value = selectedDeptForMembers;
+ document.getElementById('approvalAuthorityForm').reset();
+ document.getElementById('approvalOrder').value = '1';
+
+ // Populate approver dropdown
+ const sel = document.getElementById('approvalUserId');
+ sel.innerHTML = '
';
+ let availUsers = users;
+ if (availUsers && availUsers.length) {
+ availUsers.filter(u => u.is_active !== 0 && u.is_active !== false).forEach(u => {
+ sel.innerHTML += `
`;
+ });
+ }
+
+ document.getElementById('approvalAuthorityModal').classList.remove('hidden');
+}
+function closeApprovalModal() { document.getElementById('approvalAuthorityModal').classList.add('hidden'); }
+
+document.getElementById('approvalAuthorityForm').addEventListener('submit', async e => {
+ e.preventDefault();
+ const deptId = document.getElementById('approvalDeptId').value;
+ try {
+ await api(`/departments/${deptId}/approval-authorities`, { method: 'POST', body: JSON.stringify({
+ approval_type: document.getElementById('approvalType').value,
+ approver_user_id: parseInt(document.getElementById('approvalUserId').value),
+ approval_order: parseInt(document.getElementById('approvalOrder').value) || 1
+ })});
+ showToast('승인권한이 추가되었습니다.');
+ closeApprovalModal();
+ await loadApprovalAuthorities(parseInt(deptId));
+ } catch (e) { showToast(e.message, 'error'); }
+});
+
+async function deleteApprovalAuthority(deptId, authId) {
+ if (!confirm('이 승인권한을 삭제하시겠습니까?')) return;
+ try {
+ await api(`/departments/${deptId}/approval-authorities/${authId}`, { method: 'DELETE' });
+ showToast('삭제되었습니다.');
+ await loadApprovalAuthorities(deptId);
+ } catch (e) { showToast(e.message, 'error'); }
+}
diff --git a/user-management/web/static/js/tkuser-tabs.js b/user-management/web/static/js/tkuser-tabs.js
index d1b3162..8dc8aac 100644
--- a/user-management/web/static/js/tkuser-tabs.js
+++ b/user-management/web/static/js/tkuser-tabs.js
@@ -38,4 +38,5 @@ function switchTab(name, event) {
if (name === 'consumables' && !consumablesLoaded) loadConsumablesTab();
if (name === 'notificationRecipients' && !nrLoaded) loadNotificationRecipientsTab();
if (name === 'equipments' && !equipmentsLoaded) loadEquipmentsTab();
+ if (name === 'vacationSettings' && typeof loadVacationSettingsTab === 'function') loadVacationSettingsTab();
}
diff --git a/user-management/web/static/js/tkuser-users.js b/user-management/web/static/js/tkuser-users.js
index 3bbd65b..41d0668 100644
--- a/user-management/web/static/js/tkuser-users.js
+++ b/user-management/web/static/js/tkuser-users.js
@@ -89,6 +89,7 @@ const TKUSER_PAGES = {
{ key: 'tkuser.issue_types', title: '이슈 유형 관리', icon: 'fa-exclamation-triangle', def: false, tab: 'issueTypes' },
{ key: 'tkuser.tasks', title: '작업 관리', icon: 'fa-tasks', def: false, tab: 'tasks' },
{ key: 'tkuser.vacations', title: '휴가 관리', icon: 'fa-umbrella-beach', def: false, tab: 'vacations' },
+ { key: 'tkuser.vacation_settings', title: '연차 설정', icon: 'fa-sliders', def: false, tab: 'vacationSettings' },
{ key: 'tkuser.partners', title: '협력업체 관리', icon: 'fa-truck', def: false, tab: 'partners' },
{ key: 'tkuser.notification_recipients', title: '알림 수신자 관리', icon: 'fa-bell', def: false, tab: 'notificationRecipients' },
]
diff --git a/user-management/web/static/js/tkuser-vacation-settings.js b/user-management/web/static/js/tkuser-vacation-settings.js
new file mode 100644
index 0000000..9606dd7
--- /dev/null
+++ b/user-management/web/static/js/tkuser-vacation-settings.js
@@ -0,0 +1,158 @@
+/* ===== 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 = `
+
+
설정을 불러올 수 없습니다
+
${escHtml(e.message)}
+
`;
+ }
+}
+
+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 = `
+
+
+
+
기본 연차 설정
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
장기근속 보너스
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${isAdmin ? `
+
+
+
` : ''}
+
`;
+}
+
+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'); }
+}
diff --git a/user-management/web/static/js/tkuser-vacations.js b/user-management/web/static/js/tkuser-vacations.js
index eabff3d..52798d9 100644
--- a/user-management/web/static/js/tkuser-vacations.js
+++ b/user-management/web/static/js/tkuser-vacations.js
@@ -1,5 +1,13 @@
/* ===== Vacation CRUD ===== */
-let vacTypes = [], vacBalances = [], vacationsLoaded = false, vacWorkers = [];
+let vacTypes = [], vacBalances = [], vacationsLoaded = false, vacUsers = [];
+
+const BALANCE_TYPE_STYLES = {
+ AUTO: { label: '기본연차', cls: 'bg-blue-50 text-blue-600' },
+ MANUAL: { label: '추가부여', cls: 'bg-purple-50 text-purple-600' },
+ CARRY_OVER: { label: '이월연차', cls: 'bg-orange-50 text-orange-600' },
+ LONG_SERVICE: { label: '장기근속', cls: 'bg-emerald-50 text-emerald-600' },
+ COMPANY_GRANT: { label: '회사부여', cls: 'bg-yellow-50 text-yellow-600' },
+};
async function loadVacationsTab() {
// 연도 셀렉트 초기화
@@ -14,7 +22,8 @@ async function loadVacationsTab() {
}
}
await loadVacTypes();
- await loadVacWorkers();
+ await loadVacUsers();
+ populateVacDeptFilter();
await loadVacBalances();
vacationsLoaded = true;
}
@@ -27,11 +36,25 @@ async function loadVacTypes() {
} catch(e) { console.warn('휴가 유형 로드 실패:', e); }
}
-async function loadVacWorkers() {
+async function loadVacUsers() {
try {
- const r = await api('/workers');
- vacWorkers = (r.data || []).filter(w => w.status !== 'inactive');
- } catch(e) { console.warn('작업자 로드 실패:', e); }
+ const r = await api('/users');
+ vacUsers = ((r.data || r) || []).filter(u => u.is_active !== 0 && u.is_active !== false);
+ } catch(e) { console.warn('사용자 로드 실패:', e); }
+}
+
+function populateVacDeptFilter() {
+ const sel = document.getElementById('vacDeptFilter');
+ if (!sel) return;
+ sel.innerHTML = '
';
+ const depts = departmentsCache && departmentsCache.length ? departmentsCache : [];
+ depts.forEach(d => {
+ sel.innerHTML += `
`;
+ });
+}
+
+function filterVacBalances() {
+ renderVacBalanceTable();
}
function renderVacTypeSidebar() {
@@ -118,7 +141,12 @@ async function loadVacBalances() {
const year = document.getElementById('vacYear')?.value || new Date().getFullYear();
try {
const r = await api(`/vacations/balances/year/${year}`);
- vacBalances = r.data || [];
+ // 필드명 정규화 (과도기 호환: worker_id → user_id)
+ vacBalances = (r.data || []).map(b => ({
+ ...b,
+ user_id: b.user_id || b.worker_id,
+ user_name: b.user_name || b.worker_name,
+ }));
renderVacBalanceTable();
} catch(e) {
document.getElementById('vacBalanceTable').innerHTML = `
`;
@@ -135,30 +163,55 @@ function toggleVacDept(deptName) {
if (icon) icon.style.transform = vacDeptCollapsed[deptName] ? 'rotate(-90deg)' : '';
}
+function showLongServiceCheckbox(user) {
+ // 백엔드 플래그 우선
+ if ('long_service_excluded' in user) return true;
+ // fallback: hire_date 기준 5년 계산
+ if (!user.hire_date) return false;
+ const years = (Date.now() - new Date(user.hire_date).getTime()) / (365.25 * 24 * 60 * 60 * 1000);
+ return years >= 5;
+}
+
function renderVacBalanceTable() {
const c = document.getElementById('vacBalanceTable');
- if (!vacBalances.length) {
+
+ // Apply filters
+ const deptFilter = document.getElementById('vacDeptFilter')?.value || '';
+ const searchTerm = (document.getElementById('vacSearch')?.value || '').toLowerCase().trim();
+
+ let filtered = [...vacBalances];
+ if (deptFilter) filtered = filtered.filter(b => String(b.department_id) === deptFilter);
+ if (searchTerm) filtered = filtered.filter(b => (b.user_name || '').toLowerCase().includes(searchTerm));
+
+ // Hide LONG_SERVICE with total_days <= 0 or long_service_excluded
+ filtered = filtered.filter(b => {
+ if (b.balance_type === 'LONG_SERVICE' && (parseFloat(b.total_days) <= 0 || b.long_service_excluded === true)) return false;
+ return true;
+ });
+
+ if (!filtered.length) {
c.innerHTML = '
배정된 연차가 없습니다. "자동 계산" 또는 "개별 배정"을 이용하세요.
';
return;
}
- // 부서 -> 작업자 -> 휴가유형 그룹핑
+ // 부서 -> 사용자 -> 휴가유형 그룹핑
const deptMap = {};
- vacBalances.forEach(b => {
+ filtered.forEach(b => {
const deptName = b.department_name || '미배정';
if (!deptMap[deptName]) deptMap[deptName] = {};
- if (!deptMap[deptName][b.worker_id]) deptMap[deptName][b.worker_id] = { name: b.worker_name, hire_date: b.hire_date, items: [] };
- deptMap[deptName][b.worker_id].items.push(b);
+ const uid = b.user_id;
+ if (!deptMap[deptName][uid]) deptMap[deptName][uid] = { name: b.user_name, hire_date: b.hire_date, department_id: b.department_id, long_service_excluded: b.long_service_excluded, items: [] };
+ deptMap[deptName][uid].items.push(b);
});
const deptNames = Object.keys(deptMap).sort((a, b) => a === '미배정' ? 1 : b === '미배정' ? -1 : a.localeCompare(b));
let html = '
';
deptNames.forEach(deptName => {
- const workers = deptMap[deptName];
- const workerCount = Object.keys(workers).length;
+ const usersMap = deptMap[deptName];
+ const userCount = Object.keys(usersMap).length;
// 부서 합계
let dTotal = 0, dUsed = 0;
- Object.values(workers).forEach(g => g.items.forEach(b => { dTotal += parseFloat(b.total_days) || 0; dUsed += parseFloat(b.used_days) || 0; }));
+ Object.values(usersMap).forEach(g => g.items.forEach(b => { dTotal += parseFloat(b.total_days) || 0; dUsed += parseFloat(b.used_days) || 0; }));
const dRemain = dTotal - dUsed;
const usagePct = dTotal > 0 ? Math.round((dUsed / dTotal) * 100) : 0;
const barColor = usagePct >= 80 ? 'bg-red-400' : usagePct >= 50 ? 'bg-amber-400' : 'bg-emerald-400';
@@ -171,7 +224,7 @@ function renderVacBalanceTable() {
${escHtml(deptName)}
- ${workerCount}명
+ ${userCount}명
@@ -188,7 +241,7 @@ function renderVacBalanceTable() {
// 테이블 본문
html += `
`;
html += '
';
- html += '| 작업자 | ';
+ html += '사용자 | ';
html += '입사일 | ';
html += '휴가유형 | ';
html += '배정 | ';
@@ -198,16 +251,25 @@ function renderVacBalanceTable() {
html += ' | ';
html += '
';
- Object.values(workers).forEach(g => {
+ Object.entries(usersMap).forEach(([uid, g]) => {
+ const showLsCheckbox = showLongServiceCheckbox(g);
g.items.forEach((b, i) => {
const remaining = parseFloat(b.remaining_days || (b.total_days - b.used_days));
const remClass = remaining <= 0 ? 'text-red-500 font-semibold' : remaining <= 3 ? 'text-amber-500 font-medium' : 'text-emerald-600';
+ const btStyle = b.balance_type && BALANCE_TYPE_STYLES[b.balance_type] ? BALANCE_TYPE_STYLES[b.balance_type] : null;
html += ``;
if (i === 0) {
- html += `| ${escHtml(g.name)} | `;
- html += `${g.hire_date ? new Date(g.hire_date).toISOString().substring(0,10) : '-'} | `;
+ const rowspan = g.items.length;
+ html += `
+ ${escHtml(g.name)}
+ ${showLsCheckbox ? `` : ''}
+ | `;
+ html += `${g.hire_date ? new Date(g.hire_date).toISOString().substring(0,10) : '-'} | `;
}
- html += `${escHtml(b.type_name)} | `;
+ html += `
+ ${escHtml(b.type_name)}
+ ${btStyle ? `${btStyle.label}` : ''}
+ | `;
html += `${b.total_days} | `;
html += `${b.used_days} | `;
html += `${remaining} | `;
@@ -225,43 +287,93 @@ function renderVacBalanceTable() {
c.innerHTML = html;
}
+// 장기근속 제외 토글
+async function toggleLongServiceExclusion(userId, excluded) {
+ try {
+ await api('/vacations/long-service-exclusion', { method: 'PUT', body: JSON.stringify({ user_id: userId, excluded: excluded }) });
+ showToast(excluded ? '장기근속 제외 설정됨' : '장기근속 제외 해제됨');
+ await loadVacBalances();
+ } catch (e) {
+ showToast(e.message, 'error');
+ await loadVacBalances(); // revert checkbox state
+ }
+}
+
// 자동 계산
async function autoCalcVacation() {
const year = document.getElementById('vacYear')?.value || new Date().getFullYear();
- if (!confirm(`${year}년 전체 작업자 연차를 입사일 기준으로 자동 계산합니다.\n기존 배정이 있으면 덮어씁니다. 진행하시겠습니까?`)) return;
+ const deptFilter = document.getElementById('vacDeptFilter')?.value || '';
+ let msg = `${year}년 전체 사용자 연차를 입사일 기준으로 자동 계산합니다.\n기존 배정이 있으면 덮어씁니다. 진행하시겠습니까?`;
+ if (deptFilter) {
+ const deptName = document.getElementById('vacDeptFilter')?.selectedOptions?.[0]?.textContent || '';
+ msg = `${year}년 "${deptName}" 부서 사용자 연차를 자동 계산합니다.\n진행하시겠습니까?`;
+ }
+ if (!confirm(msg)) return;
try {
- const r = await api('/vacations/balances/auto-calculate', { method: 'POST', body: JSON.stringify({ year: parseInt(year) }) });
- showToast(`${r.data.count}명 자동 배정 완료`);
+ const body = { year: parseInt(year) };
+ if (deptFilter) body.department_id = parseInt(deptFilter);
+ const r = await api('/vacations/balances/auto-calculate', { method: 'POST', body: JSON.stringify(body) });
+ const count = (r.data && (r.data.calculated || r.data.count)) || 0;
+ const skipped = (r.data && r.data.skipped) || 0;
+ showToast(`${count}명 자동 배정 완료${skipped ? `, ${skipped}명 건너뜀` : ''}`);
await loadVacBalances();
} catch(e) { showToast(e.message, 'error'); }
}
+// balance_type 변경 시 expires_at 기본값 조정
+function onBalanceTypeChange() {
+ const bt = document.getElementById('vbBalanceType').value;
+ const expiresInput = document.getElementById('vbExpiresAt');
+ const year = document.getElementById('vacYear')?.value || new Date().getFullYear();
+
+ if (bt === 'LONG_SERVICE') {
+ expiresInput.disabled = true;
+ expiresInput.value = '';
+ } else {
+ expiresInput.disabled = false;
+ if (bt === 'CARRY_OVER') {
+ // 해당연 2월말
+ const febEnd = new Date(parseInt(year), 2, 0); // month=2, day=0 = last day of Feb
+ expiresInput.value = febEnd.toISOString().substring(0, 10);
+ } else {
+ // 연말
+ expiresInput.value = `${year}-12-31`;
+ }
+ }
+}
+
// 개별 배정 모달
function openVacBalanceModal(editId) {
document.getElementById('vbEditId').value = '';
document.getElementById('vacBalanceForm').reset();
document.getElementById('vbTotalDays').value = '0';
document.getElementById('vbUsedDays').value = '0';
+ document.getElementById('vbBalanceType').value = 'AUTO';
+ document.getElementById('vbExpiresAt').disabled = false;
document.getElementById('vacBalModalTitle').textContent = '연차 배정';
- // 작업자 셀렉트
- const wSel = document.getElementById('vbWorker');
- wSel.innerHTML = '';
+
+ const year = document.getElementById('vacYear')?.value || new Date().getFullYear();
+ document.getElementById('vbExpiresAt').value = `${year}-12-31`;
+
+ // 사용자 셀렉트 (부서별 optgroup)
+ const uSel = document.getElementById('vbUser');
+ uSel.innerHTML = '';
const byDept = {};
- vacWorkers.forEach(w => {
- const dept = w.department_name || '부서 미지정';
+ vacUsers.forEach(u => {
+ const dept = u.department_name || '부서 미지정';
if (!byDept[dept]) byDept[dept] = [];
- byDept[dept].push(w);
+ byDept[dept].push(u);
});
Object.keys(byDept).sort().forEach(dept => {
const group = document.createElement('optgroup');
group.label = dept;
- byDept[dept].forEach(w => {
+ byDept[dept].forEach(u => {
const o = document.createElement('option');
- o.value = w.worker_id;
- o.textContent = w.worker_name;
+ o.value = u.id;
+ o.textContent = u.name || u.username;
group.appendChild(o);
});
- wSel.appendChild(group);
+ uSel.appendChild(group);
});
// 유형 셀렉트
const tSel = document.getElementById('vbType');
@@ -272,23 +384,27 @@ function openVacBalanceModal(editId) {
if (!b) return;
document.getElementById('vacBalModalTitle').textContent = '배정 수정';
document.getElementById('vbEditId').value = b.id;
- wSel.value = b.worker_id;
- wSel.disabled = true;
+ uSel.value = b.user_id;
+ uSel.disabled = true;
tSel.value = b.vacation_type_id;
tSel.disabled = true;
+ if (b.balance_type) document.getElementById('vbBalanceType').value = b.balance_type;
document.getElementById('vbTotalDays').value = b.total_days;
document.getElementById('vbUsedDays').value = b.used_days;
+ if (b.expires_at) document.getElementById('vbExpiresAt').value = b.expires_at.substring(0, 10);
document.getElementById('vbNotes').value = b.notes || '';
+ if (b.balance_type === 'LONG_SERVICE') document.getElementById('vbExpiresAt').disabled = true;
} else {
- wSel.disabled = false;
+ uSel.disabled = false;
tSel.disabled = false;
}
document.getElementById('vacBalanceModal').classList.remove('hidden');
}
function closeVacBalanceModal() {
document.getElementById('vacBalanceModal').classList.add('hidden');
- document.getElementById('vbWorker').disabled = false;
+ document.getElementById('vbUser').disabled = false;
document.getElementById('vbType').disabled = false;
+ document.getElementById('vbExpiresAt').disabled = false;
}
function editVacBalance(id) { openVacBalanceModal(id); }
@@ -300,17 +416,21 @@ document.getElementById('vacBalanceForm').addEventListener('submit', async e =>
await api(`/vacations/balances/${editId}`, { method: 'PUT', body: JSON.stringify({
total_days: parseFloat(document.getElementById('vbTotalDays').value) || 0,
used_days: parseFloat(document.getElementById('vbUsedDays').value) || 0,
+ balance_type: document.getElementById('vbBalanceType').value,
+ expires_at: document.getElementById('vbExpiresAt').value || null,
notes: document.getElementById('vbNotes').value.trim() || null
})});
showToast('수정되었습니다.');
} else {
const year = document.getElementById('vacYear')?.value || new Date().getFullYear();
await api('/vacations/balances', { method: 'POST', body: JSON.stringify({
- worker_id: parseInt(document.getElementById('vbWorker').value),
+ user_id: parseInt(document.getElementById('vbUser').value),
vacation_type_id: parseInt(document.getElementById('vbType').value),
year: parseInt(year),
total_days: parseFloat(document.getElementById('vbTotalDays').value) || 0,
used_days: parseFloat(document.getElementById('vbUsedDays').value) || 0,
+ balance_type: document.getElementById('vbBalanceType').value,
+ expires_at: document.getElementById('vbExpiresAt').value || null,
notes: document.getElementById('vbNotes').value.trim() || null
})});
showToast('배정되었습니다.');