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:
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user