feat: [payroll] 전월 급여 복사 등록 기능 추가
- PayrollService에 copyFromPreviousMonth() 메서드 추가 - PayrollController에 copyFromPrevious() 액션 추가 - 전월 지급/공제 금액을 그대로 복사 (요율 재계산 없음) - 이미 존재하는 사원/연월은 스킵 처리
This commit is contained in:
@@ -271,6 +271,45 @@ public function pay(Request $request, int $id): JsonResponse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 전월 급여 복사 등록
|
||||
*/
|
||||
public function copyFromPrevious(Request $request): JsonResponse
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'pay_year' => 'required|integer|min:2020|max:2100',
|
||||
'pay_month' => 'required|integer|min:1|max:12',
|
||||
]);
|
||||
|
||||
try {
|
||||
$result = $this->payrollService->copyFromPreviousMonth($validated['pay_year'], $validated['pay_month']);
|
||||
|
||||
if (! empty($result['no_previous'])) {
|
||||
$prevYear = $validated['pay_month'] === 1 ? $validated['pay_year'] - 1 : $validated['pay_year'];
|
||||
$prevMonth = $validated['pay_month'] === 1 ? 12 : $validated['pay_month'] - 1;
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => "{$prevYear}년 {$prevMonth}월 급여 데이터가 없습니다.",
|
||||
], 422);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => "전월 복사 완료: {$result['created']}건 생성, {$result['skipped']}건 건너뜀 (이미 존재).",
|
||||
'data' => $result,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '전월 복사 중 오류가 발생했습니다.',
|
||||
'error' => config('app.debug') ? $e->getMessage() : null,
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 생성
|
||||
*/
|
||||
|
||||
@@ -302,6 +302,74 @@ public function payPayroll(int $id): ?Payroll
|
||||
return $payroll->fresh(['user']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 전월 급여 복사 등록
|
||||
*/
|
||||
public function copyFromPreviousMonth(int $year, int $month): array
|
||||
{
|
||||
$tenantId = session('selected_tenant_id', 1);
|
||||
|
||||
// 전월 계산 (1월 → 전년 12월)
|
||||
$prevYear = $month === 1 ? $year - 1 : $year;
|
||||
$prevMonth = $month === 1 ? 12 : $month - 1;
|
||||
|
||||
$previousPayrolls = Payroll::query()
|
||||
->forTenant($tenantId)
|
||||
->forPeriod($prevYear, $prevMonth)
|
||||
->get();
|
||||
|
||||
if ($previousPayrolls->isEmpty()) {
|
||||
return ['created' => 0, 'skipped' => 0, 'no_previous' => true];
|
||||
}
|
||||
|
||||
$created = 0;
|
||||
$skipped = 0;
|
||||
|
||||
DB::transaction(function () use ($previousPayrolls, $tenantId, $year, $month, &$created, &$skipped) {
|
||||
foreach ($previousPayrolls as $prev) {
|
||||
$exists = Payroll::query()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('user_id', $prev->user_id)
|
||||
->forPeriod($year, $month)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
$skipped++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Payroll::create([
|
||||
'tenant_id' => $tenantId,
|
||||
'user_id' => $prev->user_id,
|
||||
'pay_year' => $year,
|
||||
'pay_month' => $month,
|
||||
'base_salary' => $prev->base_salary,
|
||||
'overtime_pay' => $prev->overtime_pay,
|
||||
'bonus' => $prev->bonus,
|
||||
'allowances' => $prev->allowances,
|
||||
'gross_salary' => $prev->gross_salary,
|
||||
'income_tax' => $prev->income_tax,
|
||||
'resident_tax' => $prev->resident_tax,
|
||||
'health_insurance' => $prev->health_insurance,
|
||||
'long_term_care' => $prev->long_term_care,
|
||||
'pension' => $prev->pension,
|
||||
'employment_insurance' => $prev->employment_insurance,
|
||||
'deductions' => $prev->deductions,
|
||||
'total_deductions' => $prev->total_deductions,
|
||||
'net_salary' => $prev->net_salary,
|
||||
'status' => 'draft',
|
||||
'created_by' => auth()->id(),
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
|
||||
$created++;
|
||||
}
|
||||
});
|
||||
|
||||
return ['created' => $created, 'skipped' => $skipped];
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 생성 (재직 사원 전체)
|
||||
*/
|
||||
|
||||
@@ -36,6 +36,13 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-70
|
||||
</svg>
|
||||
일괄 생성
|
||||
</button>
|
||||
<button type="button" onclick="copyFromPreviousMonth()"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"/>
|
||||
</svg>
|
||||
전월 복사
|
||||
</button>
|
||||
<button type="button" onclick="exportPayrolls()"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -940,6 +947,38 @@ function bulkGeneratePayrolls() {
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 전월 복사 =====
|
||||
function copyFromPreviousMonth() {
|
||||
const year = document.getElementById('payrollYear').value;
|
||||
const month = document.getElementById('payrollMonth').value;
|
||||
|
||||
if (!confirm(`${year}년 ${month}월 급여를 전월 데이터로 복사 등록하시겠습니까?`)) return;
|
||||
|
||||
fetch('{{ route("api.admin.hr.payrolls.copy-from-previous") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ pay_year: parseInt(year), pay_month: parseInt(month) }),
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
showToast(result.message, 'success');
|
||||
refreshPayrollTable();
|
||||
refreshStats();
|
||||
} else {
|
||||
showToast(result.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('전월 복사 중 오류가 발생했습니다.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 급여 상세 모달 =====
|
||||
function openPayrollDetail(id, data) {
|
||||
let html = '<div class="space-y-4">';
|
||||
|
||||
@@ -1120,6 +1120,7 @@
|
||||
Route::get('/stats', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'stats'])->name('stats');
|
||||
Route::get('/export', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'export'])->name('export');
|
||||
Route::post('/bulk-generate', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'bulkGenerate'])->name('bulk-generate');
|
||||
Route::post('/copy-from-previous', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'copyFromPrevious'])->name('copy-from-previous');
|
||||
Route::post('/calculate', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'calculate'])->name('calculate');
|
||||
Route::get('/', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'index'])->name('index');
|
||||
Route::post('/', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'store'])->name('store');
|
||||
|
||||
Reference in New Issue
Block a user