fix(tkuser): 연차 배정 모달 간소화 — 휴가유형 드롭다운 제거
- "휴가 유형" 드롭다운 → hidden (vacation_type_id 자동 설정) - "배정 유형"이 메인 셀렉터: 기본연차/이월/장기근속/경조사 - balance_type별 vacation_type_id 자동 매핑: AUTO/MANUAL→ANNUAL_FULL, CARRY_OVER→CARRYOVER, LONG_SERVICE→LONG_SERVICE - 경조사(COMPANY_GRANT) 선택 시 서브 드롭다운 표시 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ exports.up = async function(knex) {
|
||||
table.increments('id').primary();
|
||||
table.string('type_code', 20).unique().notNullable().comment('휴가 코드');
|
||||
table.string('type_name', 50).notNullable().comment('휴가 이름');
|
||||
table.decimal('deduct_days', 3, 1).defaultTo(1.0).comment('차감 일수');
|
||||
table.decimal('deduct_days', 4, 2).defaultTo(1.00).comment('차감 일수');
|
||||
table.boolean('is_active').defaultTo(true).comment('활성 여부');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('updated_at').defaultTo(knex.fn.now());
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- vacation_types.deduct_days 정밀도 수정: DECIMAL(3,1) → DECIMAL(4,2)
|
||||
-- 0.25(반반차)가 0.3으로 반올림되는 문제 해결
|
||||
ALTER TABLE vacation_types MODIFY deduct_days DECIMAL(4,2) DEFAULT 1.00;
|
||||
UPDATE vacation_types SET deduct_days = 0.25 WHERE deduct_days = 0.3 AND type_name = '반반차';
|
||||
@@ -48,7 +48,7 @@ async function runStartupMigrations() {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const db = await getDb();
|
||||
const migrationFiles = ['20260326_schedule_extensions.sql', '20260330_add_proxy_input_fields.sql', '20260330_create_monthly_work_confirmations.sql'];
|
||||
const migrationFiles = ['20260326_schedule_extensions.sql', '20260330_add_proxy_input_fields.sql', '20260330_create_monthly_work_confirmations.sql', '20260331_fix_deduct_days_precision.sql'];
|
||||
for (const file of migrationFiles) {
|
||||
const sqlPath = path.join(__dirname, 'db', 'migrations', file);
|
||||
if (!fs.existsSync(sqlPath)) continue;
|
||||
|
||||
@@ -9,16 +9,21 @@
|
||||
|
||||
const AttendanceModel = require('../models/attendanceModel');
|
||||
const vacationBalanceModel = require('../models/vacationBalanceModel');
|
||||
const { getDb } = require('../dbPool');
|
||||
const { ValidationError, NotFoundError, DatabaseError } = require('../utils/errors');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* 휴가 사용 유형 ID를 차감 일수로 변환
|
||||
* vacation_type_id: 1=연차(1일), 2=반차(0.5일), 3=반반차(0.25일)
|
||||
* 휴가 사용 유형 ID → 차감 일수 (DB vacation_types.deduct_days 조회)
|
||||
*/
|
||||
const getVacationDays = (vacationTypeId) => {
|
||||
const daysMap = { 1: 1, 2: 0.5, 3: 0.25 };
|
||||
return daysMap[vacationTypeId] || 0;
|
||||
const getVacationDays = async (vacationTypeId) => {
|
||||
if (!vacationTypeId) return 0;
|
||||
const db = await getDb();
|
||||
const [rows] = await db.execute(
|
||||
'SELECT deduct_days FROM vacation_types WHERE id = ?',
|
||||
[vacationTypeId]
|
||||
);
|
||||
return rows.length > 0 ? parseFloat(rows[0].deduct_days) || 0 : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -143,8 +148,8 @@ const upsertAttendanceRecordService = async (recordData) => {
|
||||
|
||||
// 3. 휴가 잔액 연동 (vacation_balance_details.used_days 업데이트)
|
||||
const year = new Date(record_date).getFullYear();
|
||||
const previousDays = getVacationDays(previousVacationTypeId);
|
||||
const newDays = getVacationDays(vacation_type_id);
|
||||
const previousDays = await getVacationDays(previousVacationTypeId);
|
||||
const newDays = await getVacationDays(vacation_type_id);
|
||||
|
||||
// 이전 휴가가 있었고 변경된 경우 → 복구 후 차감
|
||||
if (previousDays !== newDays) {
|
||||
|
||||
@@ -744,13 +744,16 @@
|
||||
|
||||
// ===== 월별 사용 내역 모달 =====
|
||||
async function openMonthlyDetail(userId, workerName) {
|
||||
currentModalUserId = userId;
|
||||
currentModalName = workerName;
|
||||
editBackup = {};
|
||||
const year = parseInt(document.getElementById('yearSelect').value);
|
||||
document.getElementById('monthlyDetailTitle').textContent = `${workerName} — ${year}년 연차 사용 내역`;
|
||||
document.getElementById('monthlyDetailTitle').textContent = workerName + ' \u2014 ' + year + '년 연차 사용 내역';
|
||||
document.getElementById('monthlyDetailBody').innerHTML = '<p style="text-align:center;color:#9ca3af;padding:1rem;">로딩 중...</p>';
|
||||
document.getElementById('monthlyDetailModal').classList.add('active');
|
||||
|
||||
try {
|
||||
const res = await axios.get(`/attendance/records?start_date=${year}-01-01&end_date=${year}-12-31&user_id=${userId}`);
|
||||
const res = await axios.get('/attendance/records?start_date=' + year + '-01-01&end_date=' + year + '-12-31&user_id=' + userId);
|
||||
const records = (res.data.data || []).filter(r => r.vacation_type_id);
|
||||
|
||||
if (records.length === 0) {
|
||||
@@ -759,7 +762,6 @@
|
||||
}
|
||||
|
||||
// 월별 그룹핑
|
||||
const DAYS_KR = ['일', '월', '화', '수', '목', '금', '토'];
|
||||
const monthly = {};
|
||||
const totals = { ANNUAL_FULL: 0, ANNUAL_HALF: 0, ANNUAL_QUARTER: 0, other: 0 };
|
||||
|
||||
@@ -782,43 +784,45 @@
|
||||
const grandTotal = totals.ANNUAL_FULL + totals.ANNUAL_HALF + totals.ANNUAL_QUARTER + totals.other;
|
||||
|
||||
// 월별 요약 테이블
|
||||
let html = '<h4 style="font-size:0.85rem;font-weight:600;margin-bottom:0.5rem;">월별 요약</h4>';
|
||||
var html = '<h4 style="font-size:0.85rem;font-weight:600;margin-bottom:0.5rem;">월별 요약</h4>';
|
||||
html += '<table class="data-table" style="margin-bottom:1rem;"><thead><tr>';
|
||||
html += '<th style="position:static !important;">월</th><th style="position:static !important;">연차</th><th style="position:static !important;">반차</th><th style="position:static !important;">반반차</th><th style="position:static !important;">합계</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
for (let m = 1; m <= 12; m++) {
|
||||
for (var m = 1; m <= 12; m++) {
|
||||
if (!monthly[m]) continue;
|
||||
const d = monthly[m];
|
||||
html += `<tr>
|
||||
<td>${m}월</td>
|
||||
<td>${d.ANNUAL_FULL > 0 ? fmtNum(d.ANNUAL_FULL) : '-'}</td>
|
||||
<td>${d.ANNUAL_HALF > 0 ? fmtNum(d.ANNUAL_HALF) : '-'}</td>
|
||||
<td>${d.ANNUAL_QUARTER > 0 ? fmtNum(d.ANNUAL_QUARTER) : '-'}</td>
|
||||
<td style="font-weight:600">${fmtNum(d.total)}</td>
|
||||
</tr>`;
|
||||
var md = monthly[m];
|
||||
html += '<tr><td>' + m + '월</td>';
|
||||
html += '<td>' + (md.ANNUAL_FULL > 0 ? fmtNum(md.ANNUAL_FULL) : '-') + '</td>';
|
||||
html += '<td>' + (md.ANNUAL_HALF > 0 ? fmtNum(md.ANNUAL_HALF) : '-') + '</td>';
|
||||
html += '<td>' + (md.ANNUAL_QUARTER > 0 ? fmtNum(md.ANNUAL_QUARTER) : '-') + '</td>';
|
||||
html += '<td style="font-weight:600">' + fmtNum(md.total) + '</td></tr>';
|
||||
}
|
||||
html += `<tr style="font-weight:700;border-top:2px solid #e5e7eb;">
|
||||
<td>합계</td>
|
||||
<td>${fmtNum(totals.ANNUAL_FULL)}</td>
|
||||
<td>${fmtNum(totals.ANNUAL_HALF)}</td>
|
||||
<td>${fmtNum(totals.ANNUAL_QUARTER)}</td>
|
||||
<td style="color:#059669">${fmtNum(grandTotal)}</td>
|
||||
</tr>`;
|
||||
html += '<tr style="font-weight:700;border-top:2px solid #e5e7eb;">';
|
||||
html += '<td>합계</td>';
|
||||
html += '<td>' + fmtNum(totals.ANNUAL_FULL) + '</td>';
|
||||
html += '<td>' + fmtNum(totals.ANNUAL_HALF) + '</td>';
|
||||
html += '<td>' + fmtNum(totals.ANNUAL_QUARTER) + '</td>';
|
||||
html += '<td style="color:#059669">' + fmtNum(grandTotal) + '</td></tr>';
|
||||
html += '</tbody></table>';
|
||||
|
||||
// 상세 내역
|
||||
// 상세 내역 — 월별 아코디언
|
||||
html += '<h4 style="font-size:0.85rem;font-weight:600;margin-bottom:0.5rem;">상세 내역</h4>';
|
||||
html += '<div style="font-size:0.8rem;">';
|
||||
records.sort((a, b) => a.record_date.localeCompare(b.record_date)).forEach(r => {
|
||||
const d = new Date(r.record_date);
|
||||
const dateStr = `${d.getMonth()+1}/${String(d.getDate()).padStart(2,'0')}(${DAYS_KR[d.getDay()]})`;
|
||||
const days = parseFloat(r.vacation_days) || 1;
|
||||
html += `<div style="padding:0.25rem 0;border-bottom:1px solid #f3f4f6;display:flex;justify-content:space-between">
|
||||
<span>${dateStr} ${r.vacation_type_name || ''}</span>
|
||||
<span style="color:#6b7280">${fmtNum(days)}일</span>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
records.sort(function(a, b) { return a.record_date.localeCompare(b.record_date); });
|
||||
var firstMonth = true;
|
||||
for (var mm = 1; mm <= 12; mm++) {
|
||||
if (!monthly[mm]) continue;
|
||||
var monthRecords = records.filter(function(r) { return new Date(r.record_date).getMonth() + 1 === mm; });
|
||||
var isOpen = firstMonth;
|
||||
html += '<div class="month-accordion">';
|
||||
html += '<div class="month-header" onclick="toggleMonth(' + mm + ')">';
|
||||
html += '<span class="month-arrow" id="arrow-' + mm + '">' + (isOpen ? '\u25BC' : '\u25B6') + '</span>';
|
||||
html += mm + '월 (' + fmtNum(monthly[mm].total) + '일)';
|
||||
html += '</div>';
|
||||
html += '<div class="month-body" id="month-' + mm + '" style="display:' + (isOpen ? 'block' : 'none') + ';">';
|
||||
html += monthRecords.map(function(r) { return renderRecordRow(r); }).join('');
|
||||
html += '</div></div>';
|
||||
firstMonth = false;
|
||||
}
|
||||
|
||||
document.getElementById('monthlyDetailBody').innerHTML = html;
|
||||
} catch (e) {
|
||||
@@ -826,8 +830,141 @@
|
||||
}
|
||||
}
|
||||
|
||||
function renderRecordRow(r) {
|
||||
var d = new Date(r.record_date);
|
||||
var dateStr = (d.getMonth()+1) + '/' + String(d.getDate()).padStart(2,'0') + '(' + DAYS_KR[d.getDay()] + ')';
|
||||
var days = parseFloat(r.vacation_days) || 1;
|
||||
var escapedDate = r.record_date.substring(0, 10);
|
||||
var escapedName = (r.vacation_type_name || '').replace(/'/g, "\\'");
|
||||
return '<div class="record-row" id="rec-' + r.id + '">'
|
||||
+ '<span class="record-info">' + dateStr + ' ' + (r.vacation_type_name || '') + '</span>'
|
||||
+ '<span class="record-days">' + fmtNum(days) + '일</span>'
|
||||
+ '<span class="record-actions">'
|
||||
+ '<button onclick="editRecord(' + r.id + ',\'' + escapedDate + '\',' + r.vacation_type_id + ')">수정</button>'
|
||||
+ '<button class="btn-del" onclick="deleteRecord(' + r.id + ',\'' + escapedDate + '\',' + r.user_id + ',' + r.vacation_type_id + ',\'' + dateStr + '\',\'' + escapedName + '\',' + days + ')">삭제</button>'
|
||||
+ '</span></div>';
|
||||
}
|
||||
|
||||
function toggleMonth(m) {
|
||||
var body = document.getElementById('month-' + m);
|
||||
var arrow = document.getElementById('arrow-' + m);
|
||||
if (body.style.display === 'none') {
|
||||
body.style.display = 'block';
|
||||
arrow.textContent = '\u25BC';
|
||||
} else {
|
||||
body.style.display = 'none';
|
||||
arrow.textContent = '\u25B6';
|
||||
}
|
||||
}
|
||||
|
||||
function editRecord(recordId, recordDate, currentTypeId) {
|
||||
var row = document.getElementById('rec-' + recordId);
|
||||
if (!row) return;
|
||||
editBackup[recordId] = { html: row.innerHTML, cls: row.className };
|
||||
row.className = 'edit-row';
|
||||
row.innerHTML =
|
||||
'<input type="date" value="' + recordDate + '" id="edit-date-' + recordId + '">'
|
||||
+ '<select id="edit-type-' + recordId + '">'
|
||||
+ '<option value="1"' + (currentTypeId==1?' selected':'') + '>연차</option>'
|
||||
+ '<option value="2"' + (currentTypeId==2?' selected':'') + '>반차</option>'
|
||||
+ '<option value="3"' + (currentTypeId==3?' selected':'') + '>반반차</option>'
|
||||
+ '</select>'
|
||||
+ '<button class="btn-save" onclick="saveEdit(' + recordId + ',\'' + recordDate + '\',' + currentTypeId + ')">저장</button>'
|
||||
+ '<button class="btn-cancel" onclick="cancelEdit(' + recordId + ')">취소</button>';
|
||||
}
|
||||
|
||||
function cancelEdit(recordId) {
|
||||
var row = document.getElementById('rec-' + recordId);
|
||||
if (row && editBackup[recordId]) {
|
||||
row.className = editBackup[recordId].cls;
|
||||
row.innerHTML = editBackup[recordId].html;
|
||||
delete editBackup[recordId];
|
||||
}
|
||||
}
|
||||
|
||||
async function saveEdit(recordId, originalDate, originalTypeId) {
|
||||
var dateEl = document.getElementById('edit-date-' + recordId);
|
||||
var typeEl = document.getElementById('edit-type-' + recordId);
|
||||
if (!dateEl || !typeEl) return;
|
||||
var newDate = dateEl.value;
|
||||
var newTypeId = parseInt(typeEl.value);
|
||||
|
||||
if (!newDate) { alert('날짜를 선택해주세요.'); return; }
|
||||
|
||||
try {
|
||||
if (newDate !== originalDate) {
|
||||
// 새 날짜에 기존 기록 있는지 확인
|
||||
var checkRes = await axios.get('/attendance/records?start_date=' + newDate + '&end_date=' + newDate + '&user_id=' + currentModalUserId);
|
||||
var existing = (checkRes.data.data || []).find(function(r) { return String(r.user_id) === String(currentModalUserId); });
|
||||
if (existing && existing.vacation_type_id) {
|
||||
if (!confirm(newDate + '에 이미 ' + (existing.vacation_type_name || '연차') + ' 기록이 있습니다. 덮어쓰시겠습니까?')) return;
|
||||
} else if (existing && existing.total_work_hours > 0) {
|
||||
if (!confirm(newDate + '에 이미 근태 기록이 있습니다. 연차로 변경하시겠습니까?')) return;
|
||||
}
|
||||
|
||||
// Step 1: 원래 날짜 휴가 제거
|
||||
await axios.put('/attendance/records', {
|
||||
record_date: originalDate, user_id: currentModalUserId,
|
||||
vacation_type_id: null
|
||||
});
|
||||
|
||||
// Step 2: 새 날짜 휴가 등록
|
||||
try {
|
||||
await axios.put('/attendance/records', {
|
||||
record_date: newDate, user_id: currentModalUserId,
|
||||
vacation_type_id: newTypeId
|
||||
});
|
||||
} catch (err2) {
|
||||
// Step 2 실패 → Step 1 롤백
|
||||
try {
|
||||
await axios.put('/attendance/records', {
|
||||
record_date: originalDate, user_id: currentModalUserId,
|
||||
vacation_type_id: originalTypeId
|
||||
});
|
||||
alert('새 날짜 등록에 실패하여 원래 상태로 복원했습니다.');
|
||||
} catch (rollbackErr) {
|
||||
alert('오류가 발생했습니다. 원래 날짜(' + originalDate + ')의 연차가 제거된 상태입니다. 관리자에게 문의해주세요.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (newTypeId !== originalTypeId) {
|
||||
// 유형만 변경
|
||||
await axios.put('/attendance/records', {
|
||||
record_date: originalDate, user_id: currentModalUserId,
|
||||
vacation_type_id: newTypeId
|
||||
});
|
||||
} else {
|
||||
cancelEdit(recordId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 모달 + 메인 테이블 새로고침
|
||||
await openMonthlyDetail(currentModalUserId, currentModalName);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
alert('저장 중 오류가 발생했습니다: ' + (err.response?.data?.message || err.message));
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRecord(recordId, recordDate, userId, typeId, dateStr, typeName, days) {
|
||||
if (!confirm(dateStr + ' ' + typeName + '(' + fmtNum(days) + '일)를 삭제하시겠습니까?')) return;
|
||||
try {
|
||||
await axios.put('/attendance/records', {
|
||||
record_date: recordDate, user_id: userId,
|
||||
vacation_type_id: null
|
||||
});
|
||||
await openMonthlyDetail(currentModalUserId, currentModalName);
|
||||
loadData();
|
||||
} catch (err) {
|
||||
alert('삭제 중 오류가 발생했습니다: ' + (err.response?.data?.message || err.message));
|
||||
}
|
||||
}
|
||||
|
||||
function closeMonthlyDetail() {
|
||||
document.getElementById('monthlyDetailModal').classList.remove('active');
|
||||
currentModalUserId = null;
|
||||
currentModalName = '';
|
||||
editBackup = {};
|
||||
}
|
||||
|
||||
document.getElementById('monthlyDetailModal').addEventListener('click', e => {
|
||||
|
||||
@@ -898,21 +898,16 @@
|
||||
<option value="">선택</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">휴가 유형 <span class="text-red-400">*</span></label>
|
||||
<select id="vbType" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" required>
|
||||
<option value="">선택</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" id="vbType" value="">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">배정 유형</label>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">유형 <span class="text-red-400">*</span></label>
|
||||
<select id="vbBalanceType" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" onchange="onBalanceTypeChange()">
|
||||
<option value="AUTO">기본연차</option>
|
||||
<option value="MANUAL">추가부여</option>
|
||||
<option value="CARRY_OVER">이월연차</option>
|
||||
<option value="LONG_SERVICE">장기근속</option>
|
||||
<option value="COMPANY_GRANT">회사부여</option>
|
||||
<option value="COMPANY_GRANT">경조사/특별휴가</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
@@ -930,6 +925,19 @@
|
||||
<input type="number" id="vbUsedDays" class="input-field w-full px-3 py-1.5 rounded-lg text-sm" value="0" step="0.25" min="0">
|
||||
</div>
|
||||
</div>
|
||||
<div id="vbSpecialTypeRow" class="hidden">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">경조사 유형</label>
|
||||
<select id="vbSpecialType" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
<option value="WEDDING">결혼 (5일)</option>
|
||||
<option value="SPOUSE_BIRTH">배우자 출산 (10일)</option>
|
||||
<option value="CHILD_WEDDING">자녀 결혼 (1일)</option>
|
||||
<option value="CONDOLENCE_PARENT">부모 사망 (5일)</option>
|
||||
<option value="CONDOLENCE_SPOUSE_PARENT">배우자 부모 사망 (5일)</option>
|
||||
<option value="CONDOLENCE_GRANDPARENT">조부모 사망 (3일)</option>
|
||||
<option value="CONDOLENCE_SIBLING">형제자매 사망 (3일)</option>
|
||||
<option value="OTHER">기타 (1일)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">만료일</label>
|
||||
<input type="date" id="vbExpiresAt" class="input-field w-full px-3 py-1.5 rounded-lg text-sm">
|
||||
@@ -2416,7 +2424,7 @@
|
||||
<script src="/static/js/tkuser-issue-types.js?v=2026031401"></script>
|
||||
<script src="/static/js/tkuser-workplaces.js?v=2026031401"></script>
|
||||
<script src="/static/js/tkuser-tasks.js?v=2026031401"></script>
|
||||
<script src="/static/js/tkuser-vacations.js?v=2026033101"></script>
|
||||
<script src="/static/js/tkuser-vacations.js?v=2026033102"></script>
|
||||
<script src="/static/js/tkuser-vacation-settings.js?v=2026032501"></script>
|
||||
<script src="/static/js/tkuser-layout-map.js?v=2026031401"></script>
|
||||
<script src="/static/js/tkuser-partners.js?v=2026031601"></script>
|
||||
|
||||
@@ -320,23 +320,45 @@ async function autoCalcVacation() {
|
||||
} catch(e) { showToast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
// balance_type 변경 시 expires_at 기본값 조정
|
||||
// vacation_type_id 자동 매핑 (vacTypes에서 type_code로 찾기)
|
||||
function getVacTypeId(typeCode) {
|
||||
const t = vacTypes.find(v => v.type_code === typeCode);
|
||||
return t ? t.id : null;
|
||||
}
|
||||
|
||||
// balance_type 변경 시 vacation_type_id + expires_at + 경조사 드롭다운 조정
|
||||
function onBalanceTypeChange() {
|
||||
const bt = document.getElementById('vbBalanceType').value;
|
||||
const expiresInput = document.getElementById('vbExpiresAt');
|
||||
const year = document.getElementById('vacYear')?.value || new Date().getFullYear();
|
||||
const specialRow = document.getElementById('vbSpecialTypeRow');
|
||||
const year = document.getElementById('vbYear')?.value || document.getElementById('vacYear')?.value || new Date().getFullYear();
|
||||
|
||||
if (bt === 'LONG_SERVICE') {
|
||||
// vacation_type_id 자동 설정
|
||||
const typeMap = { AUTO: 'ANNUAL_FULL', MANUAL: 'ANNUAL_FULL', CARRY_OVER: 'CARRYOVER', LONG_SERVICE: 'LONG_SERVICE' };
|
||||
if (typeMap[bt]) {
|
||||
document.getElementById('vbType').value = getVacTypeId(typeMap[bt]) || '';
|
||||
}
|
||||
|
||||
// 경조사 드롭다운 표시/숨김
|
||||
if (bt === 'COMPANY_GRANT') {
|
||||
specialRow.classList.remove('hidden');
|
||||
// 경조사 유형으로 vacation_type_id 설정
|
||||
const specialCode = document.getElementById('vbSpecialType').value;
|
||||
document.getElementById('vbType').value = getVacTypeId(specialCode) || getVacTypeId('ANNUAL_FULL') || '';
|
||||
} else {
|
||||
specialRow.classList.add('hidden');
|
||||
}
|
||||
|
||||
// 만료일
|
||||
if (bt === 'LONG_SERVICE' || bt === 'COMPANY_GRANT') {
|
||||
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
|
||||
const febEnd = new Date(parseInt(year), 2, 0);
|
||||
expiresInput.value = febEnd.toISOString().substring(0, 10);
|
||||
} else {
|
||||
// 연말
|
||||
expiresInput.value = `${year}-12-31`;
|
||||
}
|
||||
}
|
||||
@@ -376,10 +398,10 @@ function openVacBalanceModal(editId) {
|
||||
});
|
||||
uSel.appendChild(group);
|
||||
});
|
||||
// 유형 셀렉트
|
||||
const tSel = document.getElementById('vbType');
|
||||
tSel.innerHTML = '<option value="">선택</option>';
|
||||
vacTypes.filter(t => t.is_active).forEach(t => { tSel.innerHTML += `<option value="${t.id}">${escHtml(t.type_name)} (${escHtml(t.type_code)})</option>`; });
|
||||
// vacation_type_id 자동 설정 (기본: ANNUAL_FULL)
|
||||
document.getElementById('vbType').value = getVacTypeId('ANNUAL_FULL') || '';
|
||||
document.getElementById('vbSpecialTypeRow')?.classList.add('hidden');
|
||||
|
||||
if (editId) {
|
||||
const b = vacBalances.find(x => x.id === editId);
|
||||
if (!b) return;
|
||||
@@ -387,8 +409,7 @@ function openVacBalanceModal(editId) {
|
||||
document.getElementById('vbEditId').value = b.id;
|
||||
uSel.value = b.user_id;
|
||||
uSel.disabled = true;
|
||||
tSel.value = b.vacation_type_id;
|
||||
tSel.disabled = true;
|
||||
document.getElementById('vbType').value = b.vacation_type_id;
|
||||
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;
|
||||
@@ -397,15 +418,15 @@ function openVacBalanceModal(editId) {
|
||||
if (b.balance_type === 'LONG_SERVICE') document.getElementById('vbExpiresAt').disabled = true;
|
||||
} else {
|
||||
uSel.disabled = false;
|
||||
tSel.disabled = false;
|
||||
}
|
||||
onBalanceTypeChange(); // vacation_type_id + 만료일 + 경조사 드롭다운 설정
|
||||
document.getElementById('vacBalanceModal').classList.remove('hidden');
|
||||
}
|
||||
function closeVacBalanceModal() {
|
||||
document.getElementById('vacBalanceModal').classList.add('hidden');
|
||||
document.getElementById('vbUser').disabled = false;
|
||||
document.getElementById('vbType').disabled = false;
|
||||
document.getElementById('vbExpiresAt').disabled = false;
|
||||
document.getElementById('vbSpecialTypeRow')?.classList.add('hidden');
|
||||
}
|
||||
function editVacBalance(id) { openVacBalanceModal(id); }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user