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
|
public function destroy(int $id): JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
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([
|
return response()->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
|
|||||||
@@ -214,6 +214,20 @@ public function isDeletable(): bool
|
|||||||
return $this->status === self::STATUS_DRAFT;
|
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
|
public function getStatusLabelAttribute(): string
|
||||||
{
|
{
|
||||||
return match ($this->status) {
|
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()) {
|
if (! $approval->isDeletableBy($user)) {
|
||||||
throw new \InvalidArgumentException('삭제할 수 없는 상태입니다.');
|
throw new \InvalidArgumentException('삭제 권한이 없습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 진행 중/보류 문서 삭제 시 연동 후처리 (휴가 등)
|
||||||
|
if (in_array($approval->status, [Approval::STATUS_PENDING, Approval::STATUS_ON_HOLD])) {
|
||||||
|
$this->handleApprovalDeleted($approval);
|
||||||
}
|
}
|
||||||
|
|
||||||
$approval->steps()->delete();
|
$approval->steps()->delete();
|
||||||
$approval->update(['deleted_by' => auth()->id()]);
|
$approval->update(['deleted_by' => $user->id]);
|
||||||
|
|
||||||
return $approval->delete();
|
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())
|
@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()"
|
<button onclick="copyForRedraft()"
|
||||||
class="bg-gray-600 hover:bg-gray-700 text-white px-6 py-2 rounded-lg transition text-sm font-medium">
|
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>
|
<span class="text-xs text-gray-500 ml-2">이 문서를 복사하여 새 결재를 작성합니다.</span>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@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
|
@endsection
|
||||||
|
|
||||||
@push('scripts')
|
@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');
|
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>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
|||||||
Reference in New Issue
Block a user