const { getPool } = require('../middleware/auth'); const vacationBalanceModel = { async getByUserAndYear(userId, year) { const db = getPool(); const [rows] = await db.query(` SELECT vb.*, vb.balance_type, vb.expires_at, vt.type_name, vt.type_code, vt.priority, vt.is_special, (vb.total_days - vb.used_days) as remaining_days FROM sp_vacation_balances vb INNER JOIN vacation_types vt ON vb.vacation_type_id = vt.id WHERE vb.user_id = ? AND (vb.year = ? OR (vb.balance_type = 'LONG_SERVICE' AND vb.expires_at IS NULL)) ORDER BY vt.priority ASC, vt.type_name ASC `, [userId, year]); return rows; }, async getAllByYear(year) { const db = getPool(); const [rows] = await db.query(` SELECT vb.*, vb.balance_type, su.name as user_name, su.username, su.hire_date, su.department_id, COALESCE(d.department_name, '미배정') as department_name, vt.type_name, vt.type_code, vt.priority, (vb.total_days - vb.used_days) as remaining_days FROM sp_vacation_balances vb INNER JOIN sso_users su ON vb.user_id = su.user_id INNER JOIN vacation_types vt ON vb.vacation_type_id = vt.id LEFT JOIN departments d ON su.department_id = d.department_id WHERE (vb.year = ? OR (vb.balance_type = 'LONG_SERVICE' AND vb.expires_at IS NULL)) AND su.is_active = 1 ORDER BY su.name ASC, vt.priority ASC `, [year]); return rows; }, async allocate(data) { const db = getPool(); const balanceType = data.balance_type || 'AUTO'; const expiresAt = data.expires_at || null; const [result] = await db.query(` INSERT INTO sp_vacation_balances (user_id, vacation_type_id, year, total_days, used_days, notes, created_by, balance_type, expires_at) VALUES (?, ?, ?, ?, 0, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE total_days = VALUES(total_days), notes = VALUES(notes), updated_at = NOW() `, [data.user_id, data.vacation_type_id, data.year, data.total_days, data.notes || null, data.created_by, balanceType, expiresAt]); return result; }, // 차감 우선순위: // 1. 특별휴가(배우자출산 등) — vacation_type_id가 정확히 일치하는 잔액 먼저 // 2. 이월 → 기본연차 → 추가부여 → 장기근속 → 회사부여 순서 async deductDays(userId, vacationTypeId, year, daysToDeduct, conn) { const db = conn || getPool(); const needRelease = !conn; const c = needRelease ? await db.getConnection() : db; try { if (needRelease) await c.beginTransaction(); // 1단계: 해당 vacation_type_id와 정확히 매칭되는 잔액 우선 차감 (특별휴가) const [exactMatch] = await c.query(` SELECT id, total_days, used_days, (total_days - used_days) AS remaining_days, balance_type FROM sp_vacation_balances WHERE user_id = ? AND year = ? AND vacation_type_id = ? AND (total_days - used_days) > 0 FOR UPDATE `, [userId, year, vacationTypeId]); let remaining = daysToDeduct; for (const b of exactMatch) { if (remaining <= 0) break; const toDeduct = Math.min(remaining, parseFloat(b.remaining_days)); if (toDeduct > 0) { await c.query('UPDATE sp_vacation_balances SET used_days = used_days + ?, updated_at = NOW() WHERE id = ?', [toDeduct, b.id]); remaining -= toDeduct; } } // 2단계: 남은 차감분을 우선순위 순서로 (이미 차감한 행 제외) if (remaining > 0) { const deductedIds = exactMatch.map(b => b.id); const excludeClause = deductedIds.length > 0 ? `AND id NOT IN (${Array(deductedIds.length).fill('?').join(',')})` : ''; const queryParams = [userId, year, ...deductedIds]; const [balances] = await c.query(` SELECT id, total_days, used_days, (total_days - used_days) AS remaining_days, balance_type FROM sp_vacation_balances WHERE user_id = ? AND year = ? AND (total_days - used_days) > 0 ${excludeClause} ORDER BY FIELD(balance_type, 'CARRY_OVER', 'AUTO', 'MANUAL', 'LONG_SERVICE', 'COMPANY_GRANT') FOR UPDATE `, queryParams); for (const b of balances) { if (remaining <= 0) break; const toDeduct = Math.min(remaining, parseFloat(b.remaining_days)); if (toDeduct > 0) { await c.query('UPDATE sp_vacation_balances SET used_days = used_days + ?, updated_at = NOW() WHERE id = ?', [toDeduct, b.id]); remaining -= toDeduct; } } } if (needRelease) await c.commit(); return { affectedRows: exactMatch.length }; } catch (err) { if (needRelease) await c.rollback(); throw err; } finally { if (needRelease) c.release(); } }, // 복원: 역순 (회사부여 → 장기근속 → 추가부여 → 기본연차 → 이월) async restoreDays(userId, vacationTypeId, year, daysToRestore, conn) { const db = conn || getPool(); const needRelease = !conn; const c = needRelease ? await db.getConnection() : db; try { if (needRelease) await c.beginTransaction(); const [balances] = await c.query(` SELECT id, used_days, balance_type FROM sp_vacation_balances WHERE user_id = ? AND year = ? AND used_days > 0 ORDER BY FIELD(balance_type, 'COMPANY_GRANT', 'LONG_SERVICE', 'MANUAL', 'AUTO', 'CARRY_OVER') FOR UPDATE `, [userId, year]); let remaining = daysToRestore; for (const b of balances) { if (remaining <= 0) break; const toRestore = Math.min(remaining, parseFloat(b.used_days)); if (toRestore > 0) { await c.query('UPDATE sp_vacation_balances SET used_days = used_days - ?, updated_at = NOW() WHERE id = ?', [toRestore, b.id]); remaining -= toRestore; } } if (needRelease) await c.commit(); return { affectedRows: balances.length }; } catch (err) { if (needRelease) await c.rollback(); throw err; } finally { if (needRelease) c.release(); } }, calculateAnnualLeaveDays(hireDate, targetYear) { const hire = new Date(hireDate); const targetDate = new Date(targetYear, 0, 1); const monthsDiff = (targetDate.getFullYear() - hire.getFullYear()) * 12 + (targetDate.getMonth() - hire.getMonth()); if (monthsDiff < 12) { return Math.floor(monthsDiff); } const yearsWorked = Math.floor(monthsDiff / 12); const additionalDays = Math.floor((yearsWorked - 1) / 2); return Math.min(15 + additionalDays, 25); }, async getUserHireDate(userId) { const db = getPool(); const [rows] = await db.query('SELECT hire_date FROM sso_users WHERE user_id = ?', [userId]); return rows.length > 0 ? rows[0].hire_date : null; } }; module.exports = vacationBalanceModel;