/** * Vacation Model * * vacation_types + vacation_balance_details CRUD (MariaDB) * System 1과 같은 DB를 공유 */ const { getPool } = require('./userModel'); /* ===== Vacation Types (휴가 유형) ===== */ async function getVacationTypes() { const db = getPool(); const [rows] = await db.query( 'SELECT * FROM vacation_types WHERE is_active = TRUE ORDER BY priority ASC, id ASC' ); return rows; } async function getAllVacationTypes() { const db = getPool(); const [rows] = await db.query( 'SELECT * FROM vacation_types ORDER BY priority ASC, id ASC' ); return rows; } async function getVacationTypeById(id) { const db = getPool(); const [rows] = await db.query('SELECT * FROM vacation_types WHERE id = ?', [id]); return rows[0] || null; } async function createVacationType({ type_code, type_name, deduct_days, is_special, priority, description }) { const db = getPool(); const [result] = await db.query( `INSERT INTO vacation_types (type_code, type_name, deduct_days, is_special, priority, description, is_system) VALUES (?, ?, ?, ?, ?, ?, FALSE)`, [type_code, type_name, deduct_days ?? 1.0, is_special ? 1 : 0, priority ?? 99, description || null] ); return getVacationTypeById(result.insertId); } async function updateVacationType(id, data) { const db = getPool(); const fields = []; const values = []; if (data.type_code !== undefined) { fields.push('type_code = ?'); values.push(data.type_code); } if (data.type_name !== undefined) { fields.push('type_name = ?'); values.push(data.type_name); } if (data.deduct_days !== undefined) { fields.push('deduct_days = ?'); values.push(data.deduct_days); } if (data.is_special !== undefined) { fields.push('is_special = ?'); values.push(data.is_special ? 1 : 0); } if (data.priority !== undefined) { fields.push('priority = ?'); values.push(data.priority); } if (data.description !== undefined) { fields.push('description = ?'); values.push(data.description || null); } if (data.is_active !== undefined) { fields.push('is_active = ?'); values.push(data.is_active ? 1 : 0); } if (fields.length === 0) return getVacationTypeById(id); values.push(id); await db.query(`UPDATE vacation_types SET ${fields.join(', ')} WHERE id = ?`, values); return getVacationTypeById(id); } async function deleteVacationType(id) { const db = getPool(); // 시스템 유형은 비활성화만 const type = await getVacationTypeById(id); if (type && type.is_system) { await db.query('UPDATE vacation_types SET is_active = FALSE WHERE id = ?', [id]); } else { await db.query('UPDATE vacation_types SET is_active = FALSE WHERE id = ?', [id]); } } async function updatePriorities(items) { const db = getPool(); for (const { id, priority } of items) { await db.query('UPDATE vacation_types SET priority = ? WHERE id = ?', [priority, id]); } } /* ===== Vacation Balances (연차 배정) ===== */ async function getBalancesByYear(year) { const db = getPool(); const [rows] = await db.query( `SELECT vbd.*, vt.type_code, vt.type_name, vt.deduct_days, vt.priority, w.worker_name, w.hire_date FROM vacation_balance_details vbd JOIN vacation_types vt ON vbd.vacation_type_id = vt.id JOIN workers w ON vbd.worker_id = w.worker_id WHERE vbd.year = ? ORDER BY w.worker_name ASC, vt.priority ASC`, [year] ); return rows; } async function getBalancesByWorkerYear(workerId, year) { const db = getPool(); const [rows] = await db.query( `SELECT vbd.*, vt.type_code, vt.type_name, vt.deduct_days, vt.priority FROM vacation_balance_details vbd JOIN vacation_types vt ON vbd.vacation_type_id = vt.id WHERE vbd.worker_id = ? AND vbd.year = ? ORDER BY vt.priority ASC`, [workerId, year] ); return rows; } async function getBalanceById(id) { const db = getPool(); const [rows] = await db.query( `SELECT vbd.*, vt.type_code, vt.type_name FROM vacation_balance_details vbd JOIN vacation_types vt ON vbd.vacation_type_id = vt.id WHERE vbd.id = ?`, [id] ); return rows[0] || null; } async function createBalance({ worker_id, vacation_type_id, year, total_days, used_days, notes, created_by }) { const db = getPool(); const [result] = await db.query( `INSERT INTO vacation_balance_details (worker_id, vacation_type_id, year, total_days, used_days, notes, created_by) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE total_days = VALUES(total_days), used_days = VALUES(used_days), notes = VALUES(notes)`, [worker_id, vacation_type_id, year, total_days ?? 0, used_days ?? 0, notes || null, created_by] ); return result.insertId ? getBalanceById(result.insertId) : null; } async function updateBalance(id, data) { const db = getPool(); const fields = []; const values = []; if (data.total_days !== undefined) { fields.push('total_days = ?'); values.push(data.total_days); } if (data.used_days !== undefined) { fields.push('used_days = ?'); values.push(data.used_days); } if (data.notes !== undefined) { fields.push('notes = ?'); values.push(data.notes || null); } if (fields.length === 0) return getBalanceById(id); values.push(id); await db.query(`UPDATE vacation_balance_details SET ${fields.join(', ')} WHERE id = ?`, values); return getBalanceById(id); } async function deleteBalance(id) { const db = getPool(); await db.query('DELETE FROM vacation_balance_details WHERE id = ?', [id]); } async function bulkUpsertBalances(balances) { const db = getPool(); let count = 0; for (const b of balances) { await db.query( `INSERT INTO vacation_balance_details (worker_id, vacation_type_id, year, total_days, used_days, notes, created_by) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE total_days = VALUES(total_days), notes = VALUES(notes)`, [b.worker_id, b.vacation_type_id, b.year, b.total_days ?? 0, b.used_days ?? 0, b.notes || null, b.created_by] ); count++; } return count; } /* ===== 연차 자동 계산 (근로기준법) ===== */ function calculateAnnualDays(hireDate, targetYear) { if (!hireDate) return 0; const hire = new Date(hireDate); const yearStart = new Date(targetYear, 0, 1); const monthsDiff = (yearStart.getFullYear() - hire.getFullYear()) * 12 + (yearStart.getMonth() - hire.getMonth()); if (monthsDiff < 0) return 0; if (monthsDiff < 12) { // 1년 미만: 근무 개월 수 return Math.max(0, Math.floor(monthsDiff)); } // 1년 이상: 15일 + 2년마다 1일 추가 (최대 25일) const yearsWorked = Math.floor(monthsDiff / 12); const additional = Math.floor((yearsWorked - 1) / 2); return Math.min(15 + additional, 25); } async function autoCalculateForAllWorkers(year, createdBy) { const db = getPool(); const [workers] = await db.query( 'SELECT worker_id, worker_name, hire_date FROM workers WHERE status != ? ORDER BY worker_name', ['inactive'] ); // 연차 유형 (ANNUAL_FULL) 찾기 const [types] = await db.query( "SELECT id FROM vacation_types WHERE type_code = 'ANNUAL_FULL' AND is_active = TRUE LIMIT 1" ); if (!types.length) return { count: 0, results: [] }; const annualTypeId = types[0].id; const results = []; for (const w of workers) { const days = calculateAnnualDays(w.hire_date, year); if (days > 0) { await db.query( `INSERT INTO vacation_balance_details (worker_id, vacation_type_id, year, total_days, used_days, notes, created_by) VALUES (?, ?, ?, ?, 0, ?, ?) ON DUPLICATE KEY UPDATE total_days = VALUES(total_days), notes = VALUES(notes)`, [w.worker_id, annualTypeId, year, days, `자동계산 (입사: ${w.hire_date ? w.hire_date.toISOString().substring(0,10) : ''})`, createdBy] ); results.push({ worker_id: w.worker_id, worker_name: w.worker_name, days, hire_date: w.hire_date }); } } return { count: results.length, results }; } module.exports = { getVacationTypes, getAllVacationTypes, getVacationTypeById, createVacationType, updateVacationType, deleteVacationType, updatePriorities, getBalancesByYear, getBalancesByWorkerYear, getBalanceById, createBalance, updateBalance, deleteBalance, bulkUpsertBalances, calculateAnnualDays, autoCalculateForAllWorkers };