feat: [payroll] 입사월 급여 등록 시 일할계산 자동 적용

- 사원 선택 시 입사일이 해당 급여월이면 일할계산 자동 적용
- 산식: 월액 / 해당월총일수 × 근무일수 (입사일 포함)
- 기본급, 고정연장근로수당, 식대 모두 일할계산
- 일할계산 내역 안내 배너 표시 (산식, 금액 상세)
- 자동 적용 후 수동 수정 가능
This commit is contained in:
김보곤
2026-03-12 15:34:12 +09:00
parent c92d9c45e0
commit b7a7dfd04f

View File

@@ -187,6 +187,7 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 f
data-base-salary="{{ $si['base_salary'] ?? 0 }}"
data-overtime-pay="{{ $si['fixed_overtime_pay'] ?? 0 }}"
data-meal-allowance="{{ $si['meal_allowance'] ?? 200000 }}"
data-hire-date="{{ $emp->getJsonExtraValue('hire_date', '') }}"
data-dependents="{{ 1 + collect($emp->dependents)->where('is_dependent', true)->count() }}">
{{ $emp->display_name ?? $emp->user?->name }}
</option>
@@ -206,6 +207,36 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 f
</div>
</div>
{{-- 일할계산 안내 (입사월인 경우) --}}
<div id="prorataNotice" class="hidden rounded-lg border border-amber-300 bg-amber-50 p-3">
<div class="flex items-start gap-2">
<svg class="w-4 h-4 text-amber-600 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"/>
</svg>
<div class="text-xs text-amber-800">
<p class="font-semibold mb-1">일할계산 적용</p>
<p id="prorataDesc"></p>
<table class="mt-2 w-full text-xs border border-amber-200">
<thead>
<tr class="bg-amber-100">
<th class="border border-amber-200 px-2 py-1 text-left">항목</th>
<th class="border border-amber-200 px-2 py-1 text-right">월액</th>
<th class="border border-amber-200 px-2 py-1 text-center">산식</th>
<th class="border border-amber-200 px-2 py-1 text-right">일할금액</th>
</tr>
</thead>
<tbody id="prorataTableBody"></tbody>
<tfoot>
<tr class="bg-amber-100 font-semibold">
<td class="border border-amber-200 px-2 py-1" colspan="3">합계</td>
<td class="border border-amber-200 px-2 py-1 text-right" id="prorataTotal"></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
{{-- 지급 항목 --}}
<div>
<h4 class="text-sm font-medium text-gray-700 mb-2">지급 항목</h4>
@@ -997,6 +1028,7 @@ function openCreatePayrollModal() {
document.getElementById('payrollPayMonth').value = document.getElementById('payrollMonth').value;
document.getElementById('allowancesContainer').innerHTML = '';
document.getElementById('deductionsContainer').innerHTML = '';
hideProrataNotice();
resetCalculation();
document.getElementById('payrollModal').classList.remove('hidden');
}
@@ -1055,6 +1087,58 @@ function closePayrollModal() {
document.getElementById('payrollUserId').disabled = false;
}
// ===== 일할계산 유틸 =====
function getDaysInMonth(year, month) {
return new Date(year, month, 0).getDate();
}
function checkProrata(hireDate, payYear, payMonth) {
if (!hireDate) return null;
const d = new Date(hireDate);
if (d.getFullYear() === payYear && (d.getMonth() + 1) === payMonth) {
const totalDays = getDaysInMonth(payYear, payMonth);
const workDays = totalDays - d.getDate() + 1;
return { hireDate, totalDays, workDays, hireDay: d.getDate() };
}
return null;
}
function applyProrata(amount, totalDays, workDays) {
return Math.round(amount / totalDays * workDays);
}
function showProrataNotice(prorata, baseSalary, overtimePay, mealAllowance) {
const notice = document.getElementById('prorataNotice');
const desc = document.getElementById('prorataDesc');
const tbody = document.getElementById('prorataTableBody');
const total = document.getElementById('prorataTotal');
desc.textContent = `입사일: ${prorata.hireDate} | 근무일수: ${prorata.workDays}일 / ${prorata.totalDays}일`;
const items = [
{ name: '기본급', amount: baseSalary },
{ name: '식대', amount: mealAllowance },
{ name: '고정연장근로수당', amount: overtimePay },
];
let sum = 0;
tbody.innerHTML = items.map(item => {
const pro = applyProrata(item.amount, prorata.totalDays, prorata.workDays);
sum += pro;
return `<tr>
<td class="border border-amber-200 px-2 py-1">${item.name}</td>
<td class="border border-amber-200 px-2 py-1 text-right">${numberFormat(item.amount)}</td>
<td class="border border-amber-200 px-2 py-1 text-center">${numberFormat(item.amount)}/${prorata.totalDays}*${prorata.workDays}</td>
<td class="border border-amber-200 px-2 py-1 text-right font-medium">${numberFormat(pro)}</td>
</tr>`;
}).join('');
total.textContent = numberFormat(sum);
notice.classList.remove('hidden');
}
function hideProrataNotice() {
document.getElementById('prorataNotice').classList.add('hidden');
}
// ===== 사원 선택 시 급여 산정값 자동 입력 + 가족수 표시 =====
document.getElementById('payrollUserId').addEventListener('change', function() {
const selected = this.options[this.selectedIndex];
@@ -1062,17 +1146,34 @@ function closePayrollModal() {
const fcEl = document.getElementById('calcFamilyCount');
if (fcEl) fcEl.textContent = dependents + '명';
hideProrataNotice();
if (editingPayrollId) return;
const baseSalary = parseInt(selected.dataset.baseSalary || 0);
const overtimePay = parseInt(selected.dataset.overtimePay || 0);
const mealAllowance = parseInt(selected.dataset.mealAllowance || 0);
const hireDate = selected.dataset.hireDate || '';
let finalBase = baseSalary;
let finalOT = overtimePay;
let finalMeal = mealAllowance;
if (baseSalary > 0) {
// 연봉 산정 테이블에서 계산된 값 적용
setMoneyValue(document.getElementById('payrollBaseSalary'), baseSalary);
setMoneyValue(document.getElementById('payrollOvertimePay'), overtimePay);
setMoneyValue(document.getElementById('payrollBonus'), mealAllowance);
// 일할계산 체크
const payYear = parseInt(document.getElementById('payrollPayYear').value);
const payMonth = parseInt(document.getElementById('payrollPayMonth').value);
const prorata = checkProrata(hireDate, payYear, payMonth);
if (prorata && prorata.workDays < prorata.totalDays) {
finalBase = applyProrata(baseSalary, prorata.totalDays, prorata.workDays);
finalOT = applyProrata(overtimePay, prorata.totalDays, prorata.workDays);
finalMeal = applyProrata(mealAllowance, prorata.totalDays, prorata.workDays);
showProrataNotice(prorata, baseSalary, overtimePay, mealAllowance);
}
setMoneyValue(document.getElementById('payrollBaseSalary'), finalBase);
setMoneyValue(document.getElementById('payrollOvertimePay'), finalOT);
setMoneyValue(document.getElementById('payrollBonus'), finalMeal);
} else {
// fallback: 산정 데이터 없으면 연봉에서 단순 계산
const salary = parseInt(selected.dataset.salary || 0);