diff --git a/app/Http/Controllers/Api/Admin/HR/PayrollController.php b/app/Http/Controllers/Api/Admin/HR/PayrollController.php index dfdbc267..5d9a7e03 100644 --- a/app/Http/Controllers/Api/Admin/HR/PayrollController.php +++ b/app/Http/Controllers/Api/Admin/HR/PayrollController.php @@ -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); + } + } + /** * 일괄 생성 */ diff --git a/app/Services/HR/PayrollService.php b/app/Services/HR/PayrollService.php index 8bdec39f..1fc5b300 100644 --- a/app/Services/HR/PayrollService.php +++ b/app/Services/HR/PayrollService.php @@ -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]; + } + /** * 일괄 생성 (재직 사원 전체) */ diff --git a/resources/views/hr/payrolls/index.blade.php b/resources/views/hr/payrolls/index.blade.php index a4c14a75..6fb3f9ab 100644 --- a/resources/views/hr/payrolls/index.blade.php +++ b/resources/views/hr/payrolls/index.blade.php @@ -36,6 +36,13 @@ class="inline-flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-70 일괄 생성 + + + + + 전월 복사 + @@ -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 = ''; diff --git a/routes/api.php b/routes/api.php index 0bf0bfd9..15ba42f0 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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');