feat: [approval] 결재관리 삭제 권한 기능 추가
- 관리자/슈퍼관리자 모든 상태 결재 문서 삭제 가능 - 일반 사용자는 기존대로 draft + 본인 기안만 삭제 - 진행 중 문서 삭제 시 휴가 연동 취소 처리 - 삭제 API 403 권한 검증 추가 - 상세 페이지 삭제 버튼 + 2중 확인 다이얼로그
This commit is contained in:
@@ -146,7 +146,17 @@ public function update(Request $request, int $id): JsonResponse
|
||||
public function destroy(int $id): JsonResponse
|
||||
{
|
||||
try {
|
||||
$this->service->deleteApproval($id);
|
||||
$user = auth()->user();
|
||||
$approval = $this->service->getApproval($id);
|
||||
|
||||
if (! $approval->isDeletableBy($user)) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => '삭제 권한이 없습니다.',
|
||||
], 403);
|
||||
}
|
||||
|
||||
$this->service->deleteApproval($id, $user);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
|
||||
@@ -214,6 +214,20 @@ public function isDeletable(): bool
|
||||
return $this->status === self::STATUS_DRAFT;
|
||||
}
|
||||
|
||||
public function isDeletableBy(?User $user = null): bool
|
||||
{
|
||||
if (! $user) {
|
||||
return $this->isDeletable();
|
||||
}
|
||||
|
||||
if ($user->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->status === self::STATUS_DRAFT
|
||||
&& $this->drafter_id === $user->id;
|
||||
}
|
||||
|
||||
public function getStatusLabelAttribute(): string
|
||||
{
|
||||
return match ($this->status) {
|
||||
|
||||
@@ -175,18 +175,24 @@ public function updateApproval(int $id, array $data): Approval
|
||||
}
|
||||
|
||||
/**
|
||||
* 삭제 (draft만)
|
||||
* 삭제 (일반: draft만 / 관리자: 모든 상태)
|
||||
*/
|
||||
public function deleteApproval(int $id): bool
|
||||
public function deleteApproval(int $id, ?User $user = null): bool
|
||||
{
|
||||
$approval = Approval::findOrFail($id);
|
||||
$approval = Approval::with('form')->findOrFail($id);
|
||||
$user = $user ?? auth()->user();
|
||||
|
||||
if (! $approval->isDeletable()) {
|
||||
throw new \InvalidArgumentException('삭제할 수 없는 상태입니다.');
|
||||
if (! $approval->isDeletableBy($user)) {
|
||||
throw new \InvalidArgumentException('삭제 권한이 없습니다.');
|
||||
}
|
||||
|
||||
// 진행 중/보류 문서 삭제 시 연동 후처리 (휴가 등)
|
||||
if (in_array($approval->status, [Approval::STATUS_PENDING, Approval::STATUS_ON_HOLD])) {
|
||||
$this->handleApprovalDeleted($approval);
|
||||
}
|
||||
|
||||
$approval->steps()->delete();
|
||||
$approval->update(['deleted_by' => auth()->id()]);
|
||||
$approval->update(['deleted_by' => $user->id]);
|
||||
|
||||
return $approval->delete();
|
||||
}
|
||||
@@ -738,6 +744,24 @@ private function handleApprovalRejected(Approval $approval, string $comment): vo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 삭제 시 연동 처리 (휴가 등)
|
||||
*/
|
||||
private function handleApprovalDeleted(Approval $approval): void
|
||||
{
|
||||
if (! $approval->form || $approval->form->code !== 'leave') {
|
||||
return;
|
||||
}
|
||||
|
||||
$leave = \App\Models\HR\Leave::where('approval_id', $approval->id)->first();
|
||||
if ($leave && in_array($leave->status, ['pending', 'approved'])) {
|
||||
$leave->update([
|
||||
'status' => 'cancelled',
|
||||
'updated_by' => auth()->id(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 결재 회수 시 연동 처리 (휴가 등)
|
||||
*/
|
||||
|
||||
@@ -212,7 +212,7 @@ class="bg-yellow-500 hover:bg-yellow-600 text-white px-6 py-2 rounded-lg transit
|
||||
|
||||
{{-- 복사 재기안 (완료/반려/회수 상태에서 기안자만) --}}
|
||||
@if($approval->isCopyable() && $approval->drafter_id === auth()->id())
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||
<button onclick="copyForRedraft()"
|
||||
class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
|
||||
복사하여 재기안
|
||||
@@ -220,6 +220,19 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition
|
||||
<span class="text-xs text-gray-500 ml-2">이 문서를 복사하여 새 결재를 작성합니다.</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- 삭제 (기안자: draft만 / 관리자: 모든 상태) --}}
|
||||
@if($approval->isDeletableBy(auth()->user()))
|
||||
<div class="bg-white rounded-lg shadow-sm p-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick="deleteApproval()"
|
||||
class="bg-red-600 hover:bg-red-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
|
||||
문서 삭제
|
||||
</button>
|
||||
<span class="text-xs text-gray-500">이 결재 문서를 삭제합니다. 삭제 후 복구할 수 없습니다.</span>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
@@ -402,5 +415,39 @@ class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition
|
||||
showToast('서버 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteApproval() {
|
||||
const status = '{{ $approval->status }}';
|
||||
const isActive = ['pending', 'on_hold'].includes(status);
|
||||
|
||||
if (isActive) {
|
||||
if (!confirm('이 문서는 현재 진행 중입니다.\n삭제하면 연관된 휴가 등의 처리도 취소됩니다.\n정말 삭제하시겠습니까?')) return;
|
||||
}
|
||||
|
||||
if (!confirm('결재 문서를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/approvals/{{ $approval->id }}', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
setTimeout(() => {
|
||||
location.href = '{{ route("approvals.drafts") }}';
|
||||
}, 500);
|
||||
} else {
|
||||
showToast(data.message || '삭제에 실패했습니다.', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('서버 오류가 발생했습니다.', 'error');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
Reference in New Issue
Block a user