feat: [payroll] 급여 확정 취소 기능 추가

- 확정 상태에서 작성중으로 되돌리는 기능 추가
- Model: isUnconfirmable() 상태 헬퍼 추가
- Service: unconfirmPayroll() 메서드 추가
- Controller: unconfirm() 엔드포인트 추가
- Route: POST /{id}/unconfirm 라우트 추가
- View: 확정 취소 버튼 및 JS 함수 추가
This commit is contained in:
김보곤
2026-02-27 22:17:15 +09:00
parent f922646b7b
commit 8c574088f4
6 changed files with 102 additions and 0 deletions

View File

@@ -278,6 +278,41 @@ public function confirm(Request $request, int $id): JsonResponse
}
}
/**
* 급여 확정 취소
*/
public function unconfirm(Request $request, int $id): JsonResponse
{
if ($denied = $this->checkPayrollAccess()) {
return $denied;
}
try {
$payroll = $this->payrollService->unconfirmPayroll($id);
if (! $payroll) {
return response()->json([
'success' => false,
'message' => '급여 확정을 취소할 수 없습니다.',
], 404);
}
return response()->json([
'success' => true,
'message' => '급여 확정이 취소되었습니다.',
'data' => $payroll,
]);
} catch (\Throwable $e) {
report($e);
return response()->json([
'success' => false,
'message' => '급여 확정 취소 중 오류가 발생했습니다.',
'error' => config('app.debug') ? $e->getMessage() : null,
], 500);
}
}
/**
* 급여 지급 처리
*/

View File

@@ -141,6 +141,11 @@ public function isConfirmable(): bool
return $this->status === self::STATUS_DRAFT;
}
public function isUnconfirmable(): bool
{
return $this->status === self::STATUS_CONFIRMED;
}
public function isPayable(): bool
{
return $this->status === self::STATUS_CONFIRMED;

View File

@@ -278,6 +278,31 @@ public function confirmPayroll(int $id): ?Payroll
return $payroll->fresh(['user']);
}
/**
* 급여 확정 취소 (confirmed → draft)
*/
public function unconfirmPayroll(int $id): ?Payroll
{
$tenantId = session('selected_tenant_id', 1);
$payroll = Payroll::query()
->forTenant($tenantId)
->find($id);
if (! $payroll || ! $payroll->isUnconfirmable()) {
return null;
}
$payroll->update([
'status' => Payroll::STATUS_DRAFT,
'confirmed_at' => null,
'confirmed_by' => null,
'updated_by' => auth()->id(),
]);
return $payroll->fresh(['user']);
}
/**
* 급여 지급 처리
*/

View File

@@ -888,6 +888,33 @@ function confirmPayroll(id) {
});
}
// ===== 급여 확정 취소 =====
function unconfirmPayroll(id) {
if (!confirm('이 급여의 확정을 취소하시겠습니까? 작성중 상태로 되돌아갑니다.')) return;
fetch('{{ url("/api/admin/hr/payrolls") }}/' + id + '/unconfirm', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json',
},
})
.then(r => r.json())
.then(result => {
if (result.success) {
refreshTable();
refreshStats();
showToast(result.message, 'success');
} else {
showToast(result.message, 'error');
}
})
.catch(err => {
console.error(err);
showToast('확정 취소 중 오류가 발생했습니다.', 'error');
});
}
// ===== 급여 지급 =====
function payPayroll(id) {
if (!confirm('이 급여를 지급 처리하시겠습니까?')) return;

View File

@@ -121,6 +121,15 @@
</button>
@endif
{{-- 확정 취소 (confirmed만) --}}
@if($payroll->isUnconfirmable())
<button type="button" onclick="unconfirmPayroll({{ $payroll->id }})" class="p-1 text-orange-600 hover:text-orange-800" title="확정 취소">
<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="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6"/>
</svg>
</button>
@endif
{{-- 지급 (confirmed만) --}}
@if($payroll->isPayable())
<button type="button" onclick="payPayroll({{ $payroll->id }})" class="p-1 text-emerald-600 hover:text-emerald-800" title="지급처리">

View File

@@ -1127,6 +1127,7 @@
Route::put('/{id}', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'update'])->name('update');
Route::delete('/{id}', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'destroy'])->name('destroy');
Route::post('/{id}/confirm', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'confirm'])->name('confirm');
Route::post('/{id}/unconfirm', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'unconfirm'])->name('unconfirm');
Route::post('/{id}/pay', [\App\Http\Controllers\Api\Admin\HR\PayrollController::class, 'pay'])->name('pay');
});