feat: [payroll] 입사월 급여 등록 시 일할계산 자동 적용
- 사원 선택 시 입사일이 해당 급여월이면 일할계산 자동 적용 - 산식: 월액 / 해당월총일수 × 근무일수 (입사일 포함) - 기본급, 고정연장근로수당, 식대 모두 일할계산 - 일할계산 내역 안내 배너 표시 (산식, 금액 상세) - 자동 적용 후 수동 수정 가능
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user