fix(tksupport): 전사 차감 월별 반영 + 테이블 가독성 개선 + 캘린더 차감일 표시
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,11 +51,12 @@ const vacationDashboardController = {
|
|||||||
async getYearlyOverview(req, res) {
|
async getYearlyOverview(req, res) {
|
||||||
try {
|
try {
|
||||||
const year = parseInt(req.query.year) || new Date().getFullYear();
|
const year = parseInt(req.query.year) || new Date().getFullYear();
|
||||||
const [users, balances] = await Promise.all([
|
const [users, balances, companyDeductions] = await Promise.all([
|
||||||
vacationDashboardModel.getYearlyOverview(year),
|
vacationDashboardModel.getYearlyOverview(year),
|
||||||
vacationDashboardModel.getBalances(year)
|
vacationDashboardModel.getBalances(year),
|
||||||
|
vacationDashboardModel.getCompanyDeductions(year)
|
||||||
]);
|
]);
|
||||||
res.json({ success: true, data: { users, balances } });
|
res.json({ success: true, data: { users, balances, companyDeductions } });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('연간 총괄 조회 오류:', error);
|
console.error('연간 총괄 조회 오류:', error);
|
||||||
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
res.status(500).json({ success: false, error: '서버 오류가 발생했습니다' });
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ const vacationDashboardModel = {
|
|||||||
const db = getPool();
|
const db = getPool();
|
||||||
const [rows] = await db.query(`
|
const [rows] = await db.query(`
|
||||||
SELECT
|
SELECT
|
||||||
su.user_id, su.name, su.username,
|
su.user_id, su.name, su.username, su.hire_date,
|
||||||
COALESCE(d.department_id, 0) as department_id,
|
COALESCE(d.department_id, 0) as department_id,
|
||||||
COALESCE(d.department_name, '미배정') as department_name,
|
COALESCE(d.department_name, '미배정') as department_name,
|
||||||
MONTH(vr.start_date) as month,
|
MONTH(vr.start_date) as month,
|
||||||
@@ -104,11 +104,24 @@ const vacationDashboardModel = {
|
|||||||
return rows;
|
return rows;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// View 1: 전사 휴가 차감 내역
|
||||||
|
async getCompanyDeductions(year) {
|
||||||
|
const db = getPool();
|
||||||
|
const [rows] = await db.query(`
|
||||||
|
SELECT holiday_date, holiday_name, MONTH(holiday_date) as month
|
||||||
|
FROM company_holidays
|
||||||
|
WHERE YEAR(holiday_date) = ?
|
||||||
|
AND holiday_type = 'ANNUAL_DEDUCT'
|
||||||
|
AND deduction_applied_at IS NOT NULL
|
||||||
|
`, [year]);
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
|
||||||
// View 2: 공휴일 표시용
|
// View 2: 공휴일 표시용
|
||||||
async getHolidays(year, month) {
|
async getHolidays(year, month) {
|
||||||
const db = getPool();
|
const db = getPool();
|
||||||
const [rows] = await db.query(`
|
const [rows] = await db.query(`
|
||||||
SELECT holiday_date, holiday_name
|
SELECT holiday_date, holiday_name, holiday_type, deduction_applied_at
|
||||||
FROM company_holidays
|
FROM company_holidays
|
||||||
WHERE YEAR(holiday_date) = ? AND MONTH(holiday_date) = ?
|
WHERE YEAR(holiday_date) = ? AND MONTH(holiday_date) = ?
|
||||||
`, [year, month]);
|
`, [year, month]);
|
||||||
|
|||||||
@@ -212,18 +212,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderYearlyTable(data) {
|
function renderYearlyTable(data) {
|
||||||
const { users: rows, balances: balRows } = data;
|
const { users: rows, balances: balRows, companyDeductions: deductions } = data;
|
||||||
const balMap = {};
|
const balMap = {};
|
||||||
balRows.forEach(b => { balMap[b.user_id] = { granted: parseFloat(b.granted || 0), used: parseFloat(b.used || 0) }; });
|
balRows.forEach(b => { balMap[b.user_id] = { granted: parseFloat(b.granted || 0), used: parseFloat(b.used || 0) }; });
|
||||||
|
|
||||||
|
// 전사 차감 월별 목록 (holiday_date 포함)
|
||||||
|
const deductionsByMonth = {};
|
||||||
|
(deductions || []).forEach(d => {
|
||||||
|
if (!deductionsByMonth[d.month]) deductionsByMonth[d.month] = [];
|
||||||
|
deductionsByMonth[d.month].push(d.holiday_date);
|
||||||
|
});
|
||||||
|
|
||||||
// 직원별 월 데이터 병합
|
// 직원별 월 데이터 병합
|
||||||
const empMap = {};
|
const empMap = {};
|
||||||
rows.forEach(r => {
|
rows.forEach(r => {
|
||||||
if (!empMap[r.user_id]) {
|
if (!empMap[r.user_id]) {
|
||||||
empMap[r.user_id] = { user_id: r.user_id, name: r.name, username: r.username, department_id: r.department_id, department_name: r.department_name, months: {} };
|
empMap[r.user_id] = { user_id: r.user_id, name: r.name, username: r.username, hire_date: r.hire_date, department_id: r.department_id, department_name: r.department_name, months: {} };
|
||||||
}
|
}
|
||||||
if (r.month !== null) empMap[r.user_id].months[r.month] = parseFloat(r.total_days);
|
if (r.month !== null) empMap[r.user_id].months[r.month] = parseFloat(r.total_days);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 전사 차감분 합산 (hire_date <= holiday_date 조건)
|
||||||
|
Object.values(empMap).forEach(emp => {
|
||||||
|
Object.entries(deductionsByMonth).forEach(([m, dates]) => {
|
||||||
|
dates.forEach(hDate => {
|
||||||
|
if (emp.hire_date && emp.hire_date.substring(0, 10) <= hDate.substring(0, 10)) {
|
||||||
|
emp.months[m] = (emp.months[m] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
const employees = Object.values(empMap);
|
const employees = Object.values(empMap);
|
||||||
|
|
||||||
// 부서 필터
|
// 부서 필터
|
||||||
@@ -245,12 +263,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let html = '';
|
let html = '';
|
||||||
|
let deptIdx = 0;
|
||||||
Object.entries(deptGroups).forEach(([deptId, group]) => {
|
Object.entries(deptGroups).forEach(([deptId, group]) => {
|
||||||
group.employees.forEach((emp, idx) => {
|
group.employees.forEach((emp, idx) => {
|
||||||
const bal = balMap[emp.user_id] || { granted: 0, used: 0 };
|
const bal = balMap[emp.user_id] || { granted: 0, used: 0 };
|
||||||
const remaining = bal.granted - bal.used;
|
const remaining = bal.granted - bal.used;
|
||||||
const remainClass = remaining <= 3 ? 'text-orange-600 font-bold' : 'text-gray-800';
|
const remainClass = remaining <= 3 ? 'text-orange-600 font-bold' : 'text-gray-800';
|
||||||
html += '<tr class="border-b border-gray-100">';
|
const zebraClass = idx % 2 === 1 ? ' bg-gray-50' : '';
|
||||||
|
const deptBorder = idx === 0 && deptIdx > 0 ? ' border-t-2 border-gray-300' : ' border-b border-gray-100';
|
||||||
|
html += `<tr class="${deptBorder}${zebraClass}">`;
|
||||||
if (idx === 0) {
|
if (idx === 0) {
|
||||||
html += `<td class="font-medium text-gray-700 align-top" rowspan="${group.employees.length}">${escapeHtml(group.name)}</td>`;
|
html += `<td class="font-medium text-gray-700 align-top" rowspan="${group.employees.length}">${escapeHtml(group.name)}</td>`;
|
||||||
}
|
}
|
||||||
@@ -269,6 +290,7 @@
|
|||||||
html += `<td class="text-center ${remainClass}">${remaining % 1 === 0 ? remaining : remaining.toFixed(1)}</td>`;
|
html += `<td class="text-center ${remainClass}">${remaining % 1 === 0 ? remaining : remaining.toFixed(1)}</td>`;
|
||||||
html += '</tr>';
|
html += '</tr>';
|
||||||
});
|
});
|
||||||
|
deptIdx++;
|
||||||
});
|
});
|
||||||
tbody.innerHTML = html;
|
tbody.innerHTML = html;
|
||||||
}
|
}
|
||||||
@@ -308,9 +330,14 @@
|
|||||||
const container = document.getElementById('calendarContainer');
|
const container = document.getElementById('calendarContainer');
|
||||||
const daysInMonth = new Date(year, month, 0).getDate();
|
const daysInMonth = new Date(year, month, 0).getDate();
|
||||||
const holidaySet = {};
|
const holidaySet = {};
|
||||||
|
const deductionSet = {};
|
||||||
holidays.forEach(h => {
|
holidays.forEach(h => {
|
||||||
const d = new Date(h.holiday_date).getDate();
|
const d = new Date(h.holiday_date).getDate();
|
||||||
holidaySet[d] = h.holiday_name;
|
if (h.holiday_type === 'ANNUAL_DEDUCT' && h.deduction_applied_at) {
|
||||||
|
deductionSet[d] = h.holiday_name;
|
||||||
|
} else {
|
||||||
|
holidaySet[d] = h.holiday_name;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// user_id별 그룹핑
|
// user_id별 그룹핑
|
||||||
@@ -358,9 +385,13 @@
|
|||||||
const isHoliday = holidaySet[d];
|
const isHoliday = holidaySet[d];
|
||||||
const vacType = dayVacation[d];
|
const vacType = dayVacation[d];
|
||||||
|
|
||||||
|
const isDeduction = deductionSet[d];
|
||||||
if (vacType) {
|
if (vacType) {
|
||||||
const tc = TYPE_COLOR[vacType] || DEFAULT_TYPE;
|
const tc = TYPE_COLOR[vacType] || DEFAULT_TYPE;
|
||||||
html += `<div class="cal-cell ${tc.bg} ${tc.text} font-medium" title="${d}일">${tc.label}</div>`;
|
html += `<div class="cal-cell ${tc.bg} ${tc.text} font-medium" title="${d}일">${tc.label}</div>`;
|
||||||
|
} else if (isDeduction) {
|
||||||
|
const tc = TYPE_COLOR.PAID;
|
||||||
|
html += `<div class="cal-cell ${tc.bg} ${tc.text} font-medium" title="${isDeduction}">${tc.label}</div>`;
|
||||||
} else if (isWeekend || isHoliday) {
|
} else if (isWeekend || isHoliday) {
|
||||||
html += `<div class="cal-cell weekend" title="${isHoliday || (dow === 0 ? '일' : '토')}">${d}</div>`;
|
html += `<div class="cal-cell weekend" title="${isHoliday || (dow === 0 ? '일' : '토')}">${d}</div>`;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user