feat: [hr] 슈퍼관리자 근태/신청 삭제 및 영구삭제 기능 추가
- AttendanceService: forceDeleteAttendance 메서드 추가 - LeaveService: deleteLeave(모든 상태), forceDeleteLeave 메서드 추가 - Controller: force 파라미터 + 슈퍼관리자 권한 분기 - 근태 테이블: 슈퍼관리자에게 삭제/영구삭제 버튼 표시 - 신청 테이블: 슈퍼관리자에게 삭제/영구삭제 버튼 표시
This commit is contained in:
@@ -334,7 +334,18 @@ public function update(Request $request, int $id): JsonResponse
|
||||
public function destroy(Request $request, int $id): JsonResponse|Response
|
||||
{
|
||||
try {
|
||||
$result = $this->attendanceService->deleteAttendance($id);
|
||||
$force = $request->boolean('force');
|
||||
|
||||
if ($force) {
|
||||
if (! auth()->user()->isSuperAdmin()) {
|
||||
return response()->json(['success' => false, 'message' => '권한이 없습니다.'], 403);
|
||||
}
|
||||
$result = $this->attendanceService->forceDeleteAttendance($id);
|
||||
$message = '근태가 영구 삭제되었습니다.';
|
||||
} else {
|
||||
$result = $this->attendanceService->deleteAttendance($id);
|
||||
$message = '근태가 삭제되었습니다.';
|
||||
}
|
||||
|
||||
if (! $result) {
|
||||
return response()->json([
|
||||
@@ -354,7 +365,7 @@ public function destroy(Request $request, int $id): JsonResponse|Response
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '근태가 삭제되었습니다.',
|
||||
'message' => $message,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
@@ -177,12 +177,27 @@ public function cancel(Request $request, int $id): JsonResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* pending 상태 신청 삭제
|
||||
* 신청 삭제 (일반: pending만 / 슈퍼관리자: 모든 상태, force 영구삭제)
|
||||
*/
|
||||
public function destroy(Request $request, int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$leave = $this->leaveService->deletePendingLeave($id);
|
||||
$force = $request->boolean('force');
|
||||
$isSuperAdmin = auth()->user()->isSuperAdmin();
|
||||
|
||||
if ($force) {
|
||||
if (! $isSuperAdmin) {
|
||||
return response()->json(['success' => false, 'message' => '권한이 없습니다.'], 403);
|
||||
}
|
||||
$leave = $this->leaveService->forceDeleteLeave($id);
|
||||
$message = '신청이 영구 삭제되었습니다.';
|
||||
} elseif ($isSuperAdmin) {
|
||||
$leave = $this->leaveService->deleteLeave($id);
|
||||
$message = '신청이 삭제되었습니다.';
|
||||
} else {
|
||||
$leave = $this->leaveService->deletePendingLeave($id);
|
||||
$message = '신청이 삭제되었습니다.';
|
||||
}
|
||||
|
||||
if (! $leave) {
|
||||
return response()->json([
|
||||
@@ -193,7 +208,7 @@ public function destroy(Request $request, int $id): JsonResponse
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => '신청이 삭제되었습니다.',
|
||||
'message' => $message,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
|
||||
@@ -288,6 +288,26 @@ public function deleteAttendance(int $id): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 근태 영구삭제 (슈퍼관리자 전용)
|
||||
*/
|
||||
public function forceDeleteAttendance(int $id): bool
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$attendance = Attendance::withTrashed()
|
||||
->forTenant($tenantId)
|
||||
->find($id);
|
||||
|
||||
if (! $attendance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$attendance->forceDelete();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 일괄 삭제
|
||||
*/
|
||||
|
||||
@@ -275,6 +275,89 @@ public function deletePendingLeave(int $id): ?Leave
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가/신청 삭제 (슈퍼관리자 전용 — 모든 상태 허용)
|
||||
*/
|
||||
public function deleteLeave(int $id): ?Leave
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$leave = Leave::query()
|
||||
->forTenant($tenantId)
|
||||
->find($id);
|
||||
|
||||
if (! $leave) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($leave, $tenantId) {
|
||||
// 연결된 결재 취소 시도
|
||||
if ($leave->approval_id) {
|
||||
$approval = Approval::find($leave->approval_id);
|
||||
if ($approval && in_array($approval->status, ['draft', 'pending'])) {
|
||||
try {
|
||||
app(ApprovalService::class)->cancel($approval->id);
|
||||
} catch (\Throwable $e) {
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 승인된 연차 차감 복원
|
||||
if ($leave->status === 'approved' && $leave->is_deductible) {
|
||||
$balance = LeaveBalance::query()
|
||||
->forTenant($tenantId)
|
||||
->forUser($leave->user_id)
|
||||
->forYear($leave->start_date->year)
|
||||
->first();
|
||||
$balance?->restoreLeave($leave->days);
|
||||
}
|
||||
|
||||
$leave->update(['deleted_by' => auth()->id()]);
|
||||
$leave->delete();
|
||||
|
||||
return $leave;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 휴가/신청 영구삭제 (슈퍼관리자 전용)
|
||||
*/
|
||||
public function forceDeleteLeave(int $id): ?Leave
|
||||
{
|
||||
$tenantId = session('selected_tenant_id');
|
||||
|
||||
$leave = Leave::withTrashed()
|
||||
->forTenant($tenantId)
|
||||
->find($id);
|
||||
|
||||
if (! $leave) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($leave, $tenantId) {
|
||||
// 승인된 연차 차감 복원 (아직 soft-deleted 아닌 경우)
|
||||
if ($leave->status === 'approved' && $leave->is_deductible && ! $leave->trashed()) {
|
||||
$balance = LeaveBalance::query()
|
||||
->forTenant($tenantId)
|
||||
->forUser($leave->user_id)
|
||||
->forYear($leave->start_date->year)
|
||||
->first();
|
||||
$balance?->restoreLeave($leave->days);
|
||||
}
|
||||
|
||||
// 연결된 결재 정리
|
||||
if ($leave->approval_id) {
|
||||
$approval = Approval::withTrashed()->find($leave->approval_id);
|
||||
$approval?->forceDelete();
|
||||
}
|
||||
|
||||
$leave->forceDelete();
|
||||
|
||||
return $leave;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 승인에 의한 휴가/근태신청/사유서 자동 승인
|
||||
*/
|
||||
|
||||
@@ -408,6 +408,52 @@ function deleteLeave(id) {
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
loadRequests();
|
||||
if (tabLoaded.balance) loadBalance();
|
||||
} else {
|
||||
showToast(data.message || '삭제 실패', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function forceDeleteLeave(id) {
|
||||
if (!confirm('영구 삭제하시겠습니까?\n이 작업은 되돌릴 수 없으며, 연결된 결재 기록도 함께 삭제됩니다.')) return;
|
||||
|
||||
fetch('/api/admin/hr/leaves/' + id + '?force=1', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
},
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
loadRequests();
|
||||
if (tabLoaded.balance) loadBalance();
|
||||
} else {
|
||||
showToast(data.message || '영구 삭제 실패', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAttendance(id, force) {
|
||||
const msg = force
|
||||
? '영구 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.'
|
||||
: '근태 기록을 삭제하시겠습니까?';
|
||||
if (!confirm(msg)) return;
|
||||
|
||||
const url = '/api/admin/hr/attendances/' + id + (force ? '?force=1' : '');
|
||||
fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
|
||||
},
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
loadAttendances();
|
||||
} else {
|
||||
showToast(data.message || '삭제 실패', 'error');
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
<th class="px-6 py-3 text-center text-sm font-semibold text-gray-600">퇴근</th>
|
||||
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-600">비고</th>
|
||||
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-600">GPS</th>
|
||||
@if(auth()->user()->isSuperAdmin())
|
||||
<th class="px-4 py-3 text-center text-sm font-semibold text-gray-600">관리</th>
|
||||
@endif
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-100">
|
||||
@@ -92,10 +95,29 @@ class="text-emerald-600 hover:text-emerald-800" title="GPS 정보 보기">
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
@if(auth()->user()->isSuperAdmin())
|
||||
<td class="px-4 py-4 whitespace-nowrap text-center">
|
||||
<div class="flex items-center justify-center gap-1">
|
||||
<button type="button" onclick="deleteAttendance({{ $attendance->id }}, false)"
|
||||
class="p-1 text-gray-400 hover:text-red-600 transition-colors" 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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" onclick="deleteAttendance({{ $attendance->id }}, true)"
|
||||
class="p-1 text-gray-400 hover:text-red-700 transition-colors" 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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@endif
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="8" class="px-6 py-12 text-center">
|
||||
<td colspan="{{ auth()->user()->isSuperAdmin() ? 9 : 8 }}" class="px-6 py-12 text-center">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<svg class="w-12 h-12 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
|
||||
@@ -151,6 +151,19 @@ class="px-2.5 py-1 text-xs font-medium text-red-700 bg-red-50 hover:bg-red-100 r
|
||||
@elseif(!$leave->approval_id && $leave->status !== 'pending' && $leave->status !== 'approved')
|
||||
<span class="text-xs text-gray-400">-</span>
|
||||
@endif
|
||||
|
||||
@if(auth()->user()->isSuperAdmin())
|
||||
<button type="button" onclick="deleteLeave({{ $leave->id }})"
|
||||
class="px-2.5 py-1 text-xs font-medium text-red-600 bg-red-50 hover:bg-red-100 rounded transition-colors"
|
||||
title="삭제">
|
||||
삭제
|
||||
</button>
|
||||
<button type="button" onclick="forceDeleteLeave({{ $leave->id }})"
|
||||
class="px-2.5 py-1 text-xs font-medium text-red-800 bg-red-100 hover:bg-red-200 rounded transition-colors"
|
||||
title="영구삭제 (복구 불가)">
|
||||
영구삭제
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user