feat: [approval] 결재관리 시스템 MNG 스타일로 전면 개선

- 보류/보류해제 기능 추가 (hold, releaseHold)
- 전결 기능 추가 (preDecide - 이후 결재 건너뛰고 최종 승인)
- 복사 재기안 기능 추가 (copyForRedraft)
- 반려 후 재상신 로직 (rejection_history 저장, resubmit_count 증가)
- 결재자 스냅샷 저장 (approver_name, department, position)
- 완료함 목록/현황 API 추가 (completed, completedSummary)
- 뱃지 카운트 API 추가 (badgeCounts)
- 완료함 일괄 읽음 처리 (markCompletedAsRead)
- 위임 관리 CRUD API 추가 (delegations)
- Leave 연동 (승인/반려/회수/삭제 시 휴가 상태 동기화)
- ApprovalDelegation 모델 신규 생성
- STATUS_ON_HOLD 상수 추가 (Approval, ApprovalStep)
- isEditable/isSubmittable 반려 상태 허용으로 확장
- isCancellable 보류 상태 포함
- 회수 시 첫 번째 결재자 처리 여부 검증 추가
- i18n 에러/메시지 키 추가
This commit is contained in:
김보곤
2026-03-11 16:57:54 +09:00
parent f662a389f7
commit 100a78b6e5
9 changed files with 1076 additions and 27 deletions

View File

@@ -3,10 +3,13 @@
namespace App\Services;
use App\Models\Documents\Document;
use App\Models\Members\User;
use App\Models\Tenants\Approval;
use App\Models\Tenants\ApprovalDelegation;
use App\Models\Tenants\ApprovalForm;
use App\Models\Tenants\ApprovalLine;
use App\Models\Tenants\ApprovalStep;
use App\Models\Tenants\Leave;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
@@ -626,10 +629,14 @@ public function store(array $data): Approval
'tenant_id' => $tenantId,
'document_number' => $documentNumber,
'form_id' => $form->id,
'line_id' => $data['line_id'] ?? null,
'title' => $data['title'],
'content' => $data['content'],
'body' => $data['body'] ?? null,
'status' => $status,
'is_urgent' => $data['is_urgent'] ?? false,
'drafter_id' => $userId,
'department_id' => $data['department_id'] ?? null,
'drafted_at' => $status === Approval::STATUS_PENDING ? now() : null,
'attachments' => $data['attachments'] ?? null,
'created_by' => $userId,
@@ -687,8 +694,12 @@ public function update(int $id, array $data): Approval
$approval->fill([
'form_id' => $formId,
'line_id' => $data['line_id'] ?? $approval->line_id,
'title' => $data['title'] ?? $approval->title,
'content' => $data['content'] ?? $approval->content,
'body' => $data['body'] ?? $approval->body,
'is_urgent' => $data['is_urgent'] ?? $approval->is_urgent,
'department_id' => $data['department_id'] ?? $approval->department_id,
'attachments' => $data['attachments'] ?? $approval->attachments,
'updated_by' => $userId,
]);
@@ -740,7 +751,7 @@ public function destroy(int $id): bool
}
/**
* 결재 상신
* 결재 상신 (draft → pending, rejected → pending 재상신 포함)
*/
public function submit(int $id, array $data): Approval
{
@@ -750,6 +761,7 @@ public function submit(int $id, array $data): Approval
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
$approval = Approval::query()
->where('tenant_id', $tenantId)
->with('steps')
->findOrFail($id);
if (! $approval->isSubmittable()) {
@@ -767,20 +779,48 @@ public function submit(int $id, array $data): Approval
}
}
// 먼저 approval을 pending으로 변경 (Observer가 올바른 상태로 트리거되도록)
// 반려 후 재상신 처리
$isResubmit = $approval->status === Approval::STATUS_REJECTED;
if ($isResubmit) {
// 반려 이력 저장
$rejectedStep = $approval->steps
->firstWhere('status', ApprovalStep::STATUS_REJECTED);
if ($rejectedStep) {
$history = $approval->rejection_history ?? [];
$history[] = [
'round' => $approval->resubmit_count + 1,
'approver_name' => $rejectedStep->approver_name ?? '',
'approver_position' => $rejectedStep->approver_position ?? '',
'comment' => $rejectedStep->comment,
'rejected_at' => $rejectedStep->acted_at?->format('Y-m-d H:i:s'),
];
$approval->rejection_history = $history;
}
// 기존 steps를 모두 pending으로 초기화
$approval->steps()->update([
'status' => ApprovalStep::STATUS_PENDING,
'comment' => null,
'acted_at' => null,
]);
}
// approval을 pending으로 변경
$approval->status = Approval::STATUS_PENDING;
$approval->drafted_at = now();
$approval->current_step = 1;
$approval->resubmit_count = $isResubmit
? ($approval->resubmit_count ?? 0) + 1
: ($approval->resubmit_count ?? 0);
$approval->updated_by = $userId;
$approval->save();
// steps가 있으면 새로 생성 (approval이 pending 상태일 때 생성해야 알림 발송)
// steps가 있으면 새로 생성
if (! empty($data['steps'])) {
// 기존 결재선 삭제 후 새로 생성
$approval->steps()->delete();
$this->createApprovalSteps($approval, $data['steps']);
} else {
// 기존 결재선 사용 시, Observer가 트리거되지 않으므로 수동으로 알림 발송
// 기존 결재선 사용 시 알림 발송
$firstPendingStep = $approval->steps()
->where('status', ApprovalStep::STATUS_PENDING)
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
@@ -850,6 +890,7 @@ public function approve(int $id, ?string $comment = null): Approval
// 모든 결재 완료
$approval->status = Approval::STATUS_APPROVED;
$approval->completed_at = now();
$approval->drafter_read_at = null;
}
$approval->current_step = $myStep->step_order + 1;
@@ -859,6 +900,11 @@ public function approve(int $id, ?string $comment = null): Approval
// Document 브릿지 동기화
$this->syncToLinkedDocument($approval);
// Leave 연동 (승인 완료 시)
if ($approval->status === Approval::STATUS_APPROVED) {
$this->handleApprovalCompleted($approval);
}
return $approval->fresh([
'form:id,name,code,category',
'drafter:id,name',
@@ -909,12 +955,16 @@ public function reject(int $id, string $comment): Approval
// 문서 반려 상태로 변경
$approval->status = Approval::STATUS_REJECTED;
$approval->completed_at = now();
$approval->drafter_read_at = null;
$approval->updated_by = $userId;
$approval->save();
// Document 브릿지 동기화
$this->syncToLinkedDocument($approval);
// Leave 연동 (반려 시)
$this->handleApprovalRejected($approval, $comment);
return $approval->fresh([
'form:id,name,code,category',
'drafter:id,name',
@@ -928,14 +978,14 @@ public function reject(int $id, string $comment): Approval
}
/**
* 결재 회수 (기안자만)
* 결재 회수 (기안자만, pending/on_hold → cancelled)
*/
public function cancel(int $id): Approval
public function cancel(int $id, ?string $recallReason = null): Approval
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
return DB::transaction(function () use ($id, $recallReason, $tenantId, $userId) {
$approval = Approval::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
@@ -949,24 +999,407 @@ public function cancel(int $id): Approval
throw new BadRequestHttpException(__('error.approval.only_drafter_can_cancel'));
}
// 첫 번째 결재자가 이미 처리했으면 회수 불가
$firstApproverStep = $approval->steps()
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
->orderBy('step_order')
->first();
if ($firstApproverStep
&& $firstApproverStep->status !== ApprovalStep::STATUS_PENDING
&& $firstApproverStep->status !== ApprovalStep::STATUS_ON_HOLD) {
throw new BadRequestHttpException(__('error.approval.first_approver_already_acted'));
}
// 모든 pending/on_hold steps → skipped
$approval->steps()
->whereIn('status', [ApprovalStep::STATUS_PENDING, ApprovalStep::STATUS_ON_HOLD])
->update(['status' => ApprovalStep::STATUS_SKIPPED]);
$approval->status = Approval::STATUS_CANCELLED;
$approval->completed_at = now();
$approval->recall_reason = $recallReason;
$approval->updated_by = $userId;
$approval->save();
// Document 브릿지 동기화 (steps 삭제 전에 실행)
// Document 브릿지 동기화
$this->syncToLinkedDocument($approval);
// 결재 단계들 삭제
$approval->steps()->delete();
// Leave 연동 (회수 시)
$this->handleApprovalCancelled($approval);
return $approval->fresh([
'form:id,name,code,category',
'drafter:id,name',
'steps.approver:id,name',
]);
});
}
/**
* 보류 (현재 결재자만, pending → on_hold)
*/
public function hold(int $id, string $comment): Approval
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $comment, $tenantId, $userId) {
$approval = Approval::query()
->where('tenant_id', $tenantId)
->with('steps')
->findOrFail($id);
if (! $approval->isHoldable()) {
throw new BadRequestHttpException(__('error.approval.not_holdable'));
}
$currentStep = $approval->getCurrentApproverStep();
if (! $currentStep || $currentStep->approver_id !== $userId) {
throw new BadRequestHttpException(__('error.approval.not_your_turn'));
}
$currentStep->status = ApprovalStep::STATUS_ON_HOLD;
$currentStep->comment = $comment;
$currentStep->acted_at = now();
$currentStep->save();
$approval->status = Approval::STATUS_ON_HOLD;
$approval->updated_by = $userId;
$approval->save();
return $approval->fresh([
'form:id,name,code,category',
'drafter:id,name',
'steps.approver:id,name',
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
'steps.approver.tenantProfile.department:id,name',
]);
});
}
/**
* 보류 해제 (보류한 결재자만, on_hold → pending)
*/
public function releaseHold(int $id): Approval
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
$approval = Approval::query()
->where('tenant_id', $tenantId)
->with('steps')
->findOrFail($id);
if (! $approval->isHoldReleasable()) {
throw new BadRequestHttpException(__('error.approval.not_hold_releasable'));
}
$holdStep = $approval->steps()
->where('status', ApprovalStep::STATUS_ON_HOLD)
->first();
if (! $holdStep || $holdStep->approver_id !== $userId) {
throw new BadRequestHttpException(__('error.approval.only_holder_can_release'));
}
$holdStep->status = ApprovalStep::STATUS_PENDING;
$holdStep->comment = null;
$holdStep->acted_at = null;
$holdStep->save();
$approval->status = Approval::STATUS_PENDING;
$approval->updated_by = $userId;
$approval->save();
return $approval->fresh([
'form:id,name,code,category',
'drafter:id,name',
'steps.approver:id,name',
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
'steps.approver.tenantProfile.department:id,name',
]);
});
}
/**
* 전결 (현재 결재자가 이후 모든 결재를 건너뛰고 최종 승인)
*/
public function preDecide(int $id, ?string $comment = null): Approval
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $comment, $tenantId, $userId) {
$approval = Approval::query()
->where('tenant_id', $tenantId)
->with('steps')
->findOrFail($id);
if (! $approval->isActionable()) {
throw new BadRequestHttpException(__('error.approval.not_actionable'));
}
$currentStep = $approval->getCurrentApproverStep();
if (! $currentStep || $currentStep->approver_id !== $userId) {
throw new BadRequestHttpException(__('error.approval.not_your_turn'));
}
// 현재 step → approved + pre_decided
$currentStep->status = ApprovalStep::STATUS_APPROVED;
$currentStep->approval_type = 'pre_decided';
$currentStep->comment = $comment;
$currentStep->acted_at = now();
$currentStep->save();
// 이후 모든 pending approval/agreement steps → skipped
$approval->steps()
->where('step_order', '>', $currentStep->step_order)
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
->where('status', ApprovalStep::STATUS_PENDING)
->update(['status' => ApprovalStep::STATUS_SKIPPED]);
// 문서 최종 승인
$approval->status = Approval::STATUS_APPROVED;
$approval->completed_at = now();
$approval->drafter_read_at = null;
$approval->updated_by = $userId;
$approval->save();
// Document 브릿지 동기화
$this->syncToLinkedDocument($approval);
// Leave 연동 (승인 완료)
$this->handleApprovalCompleted($approval);
return $approval->fresh([
'form:id,name,code,category',
'drafter:id,name',
'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
'drafter.tenantProfile.department:id,name',
'steps.approver:id,name',
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
'steps.approver.tenantProfile.department:id,name',
]);
});
}
/**
* 복사 재기안 (완료/반려/회수 문서를 복사하여 새 draft 생성)
*/
public function copyForRedraft(int $id): Approval
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
$original = Approval::query()
->where('tenant_id', $tenantId)
->with('steps')
->findOrFail($id);
if (! $original->isCopyable()) {
throw new BadRequestHttpException(__('error.approval.not_copyable'));
}
if ($original->drafter_id !== $userId) {
throw new BadRequestHttpException(__('error.approval.only_drafter_can_copy'));
}
$documentNumber = $this->generateDocumentNumber($tenantId);
$newApproval = Approval::create([
'tenant_id' => $tenantId,
'document_number' => $documentNumber,
'form_id' => $original->form_id,
'line_id' => $original->line_id,
'title' => $original->title,
'content' => $original->content,
'body' => $original->body,
'status' => Approval::STATUS_DRAFT,
'is_urgent' => $original->is_urgent,
'drafter_id' => $userId,
'department_id' => $original->department_id,
'current_step' => 0,
'parent_doc_id' => $original->id,
'created_by' => $userId,
'updated_by' => $userId,
]);
// 결재선 복사 (모두 pending 상태로, 스냅샷 유지)
foreach ($original->steps as $step) {
ApprovalStep::create([
'approval_id' => $newApproval->id,
'step_order' => $step->step_order,
'step_type' => $step->step_type,
'approver_id' => $step->approver_id,
'approver_name' => $step->approver_name,
'approver_department' => $step->approver_department,
'approver_position' => $step->approver_position,
'status' => ApprovalStep::STATUS_PENDING,
]);
}
return $newApproval->fresh([
'form:id,name,code,category',
'drafter:id,name',
'steps.approver:id,name',
'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name',
'steps.approver.tenantProfile.department:id,name',
]);
});
}
/**
* 완료함 - 내가 기안한 완료 문서 + 내가 결재 처리한 문서
*/
public function completed(array $params): LengthAwarePaginator
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$query = Approval::query()
->where('tenant_id', $tenantId)
->where(function ($q) use ($userId) {
$q->where(function ($sub) use ($userId) {
$sub->where('drafter_id', $userId)
->whereIn('status', [
Approval::STATUS_APPROVED,
Approval::STATUS_REJECTED,
Approval::STATUS_CANCELLED,
]);
})
->orWhereHas('steps', function ($sub) use ($userId) {
$sub->where('approver_id', $userId)
->whereIn('status', [ApprovalStep::STATUS_APPROVED, ApprovalStep::STATUS_REJECTED]);
});
})
->with([
'form:id,name,code,category',
'drafter:id,name',
'drafter.tenantProfile:id,user_id,position_key,department_id',
'drafter.tenantProfile.department:id,name',
'steps.approver:id,name',
]);
if (! empty($params['search'])) {
$query->where(function ($q) use ($params) {
$q->where('title', 'like', "%{$params['search']}%")
->orWhere('document_number', 'like', "%{$params['search']}%");
});
}
if (! empty($params['status'])) {
$query->where('status', $params['status']);
}
$sortBy = $params['sort_by'] ?? 'updated_at';
$sortDir = $params['sort_dir'] ?? 'desc';
$query->orderBy($sortBy, $sortDir);
$perPage = $params['per_page'] ?? 20;
return $query->paginate($perPage);
}
/**
* 완료함 현황 카드
*/
public function completedSummary(): array
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$myCompleted = Approval::query()
->where('tenant_id', $tenantId)
->where('drafter_id', $userId)
->whereIn('status', [Approval::STATUS_APPROVED, Approval::STATUS_REJECTED, Approval::STATUS_CANCELLED])
->selectRaw('status, COUNT(*) as count')
->groupBy('status')
->pluck('count', 'status')
->toArray();
$unreadCount = Approval::query()
->where('tenant_id', $tenantId)
->where('drafter_id', $userId)
->whereIn('status', [Approval::STATUS_APPROVED, Approval::STATUS_REJECTED])
->whereNull('drafter_read_at')
->count();
return [
'approved' => $myCompleted[Approval::STATUS_APPROVED] ?? 0,
'rejected' => $myCompleted[Approval::STATUS_REJECTED] ?? 0,
'cancelled' => $myCompleted[Approval::STATUS_CANCELLED] ?? 0,
'unread' => $unreadCount,
];
}
/**
* 미처리 건수 (뱃지용)
*/
public function badgeCounts(): array
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$pendingCount = Approval::query()
->where('tenant_id', $tenantId)
->where('status', Approval::STATUS_PENDING)
->whereHas('steps', function ($q) use ($userId) {
$q->where('approver_id', $userId)
->where('status', ApprovalStep::STATUS_PENDING)
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
->whereRaw('step_order = (SELECT MIN(s2.step_order) FROM approval_steps s2 WHERE s2.approval_id = approval_steps.approval_id AND s2.status = ?)', [ApprovalStep::STATUS_PENDING]);
})
->count();
$draftCount = Approval::query()
->where('tenant_id', $tenantId)
->where('status', Approval::STATUS_PENDING)
->where('drafter_id', $userId)
->count();
$referenceUnreadCount = ApprovalStep::query()
->where('approver_id', $userId)
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE)
->where('is_read', false)
->whereHas('approval', function ($q) use ($tenantId) {
$q->where('tenant_id', $tenantId);
})
->count();
$completedUnreadCount = Approval::query()
->where('tenant_id', $tenantId)
->where('drafter_id', $userId)
->whereIn('status', [Approval::STATUS_APPROVED, Approval::STATUS_REJECTED])
->whereNull('drafter_read_at')
->count();
return [
'pending' => $pendingCount,
'draft' => $draftCount,
'reference_unread' => $referenceUnreadCount,
'completed_unread' => $completedUnreadCount,
];
}
/**
* 완료함 미읽음 일괄 읽음 처리
*/
public function markCompletedAsRead(): int
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return Approval::query()
->where('tenant_id', $tenantId)
->where('drafter_id', $userId)
->whereIn('status', [Approval::STATUS_APPROVED, Approval::STATUS_REJECTED])
->whereNull('drafter_read_at')
->update(['drafter_read_at' => now()]);
}
/**
* Approval → Document 브릿지 동기화
* 결재 승인/반려/회수 시 연결된 Document의 상태와 결재란을 동기화
@@ -1118,12 +1551,13 @@ private function generateDocumentNumber(int $tenantId): string
}
/**
* 결재 단계 생성
* 결재 단계 생성 + 결재자 스냅샷 저장
* 프론트엔드 호환성: step_type/approver_id 또는 type/user_id 지원
* 중복 결재자 자동 제거
*/
private function createApprovalSteps(Approval $approval, array $steps): void
{
$tenantId = $this->tenantId();
$order = 1;
$processedApprovers = []; // 중복 체크용
@@ -1142,14 +1576,248 @@ private function createApprovalSteps(Approval $approval, array $steps): void
}
$processedApprovers[] = $approverId;
// 결재자 스냅샷 (이름/부서/직위)
$approverName = $step['approver_name'] ?? '';
$approverDepartment = $step['approver_department'] ?? null;
$approverPosition = $step['approver_position'] ?? null;
// 스냅샷이 비어있으면 DB에서 조회
if (empty($approverName)) {
$user = User::find($approverId);
if ($user) {
$approverName = $user->name;
$profile = $user->tenantProfile ?? $user->tenantProfiles()
->where('tenant_id', $tenantId)
->first();
if ($profile) {
$approverDepartment = $approverDepartment ?: ($profile->department?->name ?? null);
$approverPosition = $approverPosition ?: ($profile->position_label ?? $profile->job_title_label ?? null);
}
}
}
ApprovalStep::create([
'approval_id' => $approval->id,
'step_order' => $stepOrder,
'step_type' => $stepType,
'approver_id' => $approverId,
'approver_name' => $approverName,
'approver_department' => $approverDepartment,
'approver_position' => $approverPosition,
'status' => ApprovalStep::STATUS_PENDING,
]);
}
}
}
// =========================================================================
// Leave 연동 (휴가/근태신청/사유서)
// =========================================================================
/**
* 휴가/근태신청/사유서 관련 결재 양식인지 확인
*/
private function isLeaveRelatedForm(?string $code): bool
{
return in_array($code, ['leave', 'attendance_request', 'reason_report']);
}
/**
* 결재 최종 승인 시 연동 처리
*/
private function handleApprovalCompleted(Approval $approval): void
{
$approval->loadMissing('form');
if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) {
return;
}
$leave = Leave::where('approval_id', $approval->id)->first();
// 기안함에서 직접 올린 경우: Leave 레코드 자동 생성
if (! $leave && ! empty($approval->content['leave_type'])) {
$leave = $this->createLeaveFromApproval($approval);
}
if ($leave && $leave->status === 'pending') {
$leave->update([
'status' => 'approved',
'updated_by' => $approval->updated_by,
]);
}
}
/**
* 결재 반려 시 연동 처리
*/
private function handleApprovalRejected(Approval $approval, string $comment): void
{
$approval->loadMissing('form');
if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) {
return;
}
$leave = Leave::where('approval_id', $approval->id)->first();
if ($leave && $leave->status === 'pending') {
$leave->update([
'status' => 'rejected',
'updated_by' => $approval->updated_by,
]);
}
}
/**
* 결재 회수 시 연동 처리
*/
private function handleApprovalCancelled(Approval $approval): void
{
$approval->loadMissing('form');
if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) {
return;
}
$leave = Leave::where('approval_id', $approval->id)->first();
if ($leave && in_array($leave->status, ['pending', 'approved'])) {
$leave->update([
'status' => 'cancelled',
'updated_by' => $approval->updated_by,
]);
}
}
/**
* 결재 삭제 시 연동 처리
*/
private function handleApprovalDeleted(Approval $approval): void
{
$approval->loadMissing('form');
if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) {
return;
}
$leave = Leave::where('approval_id', $approval->id)->first();
if ($leave && in_array($leave->status, ['pending', 'approved'])) {
$leave->update([
'status' => 'cancelled',
'updated_by' => $this->apiUserId(),
]);
}
}
/**
* 기안함에서 직접 올린 결재 → Leave 레코드 자동 생성
*/
private function createLeaveFromApproval(Approval $approval): Leave
{
$content = $approval->content;
return Leave::create([
'tenant_id' => $approval->tenant_id,
'user_id' => $content['user_id'] ?? $approval->drafter_id,
'leave_type' => $content['leave_type'],
'start_date' => $content['start_date'],
'end_date' => $content['end_date'],
'days' => $content['days'] ?? 0,
'reason' => $content['reason'] ?? null,
'status' => 'pending',
'approval_id' => $approval->id,
'created_by' => $approval->drafter_id,
'updated_by' => $approval->drafter_id,
]);
}
// =========================================================================
// 위임 관리
// =========================================================================
/**
* 위임 목록
*/
public function delegationIndex(array $params): LengthAwarePaginator
{
$tenantId = $this->tenantId();
$query = ApprovalDelegation::query()
->where('tenant_id', $tenantId)
->with(['delegator:id,name', 'delegate:id,name']);
if (! empty($params['delegator_id'])) {
$query->where('delegator_id', $params['delegator_id']);
}
if (isset($params['is_active'])) {
$query->where('is_active', $params['is_active']);
}
$query->orderByDesc('created_at');
$perPage = $params['per_page'] ?? 20;
return $query->paginate($perPage);
}
/**
* 위임 생성
*/
public function delegationStore(array $data): ApprovalDelegation
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return ApprovalDelegation::create([
'tenant_id' => $tenantId,
'delegator_id' => $data['delegator_id'],
'delegate_id' => $data['delegate_id'],
'start_date' => $data['start_date'],
'end_date' => $data['end_date'],
'form_ids' => $data['form_ids'] ?? null,
'notify_delegator' => $data['notify_delegator'] ?? true,
'is_active' => $data['is_active'] ?? true,
'reason' => $data['reason'] ?? null,
'created_by' => $userId,
]);
}
/**
* 위임 수정
*/
public function delegationUpdate(int $id, array $data): ApprovalDelegation
{
$tenantId = $this->tenantId();
$delegation = ApprovalDelegation::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$delegation->fill([
'delegate_id' => $data['delegate_id'] ?? $delegation->delegate_id,
'start_date' => $data['start_date'] ?? $delegation->start_date,
'end_date' => $data['end_date'] ?? $delegation->end_date,
'form_ids' => array_key_exists('form_ids', $data) ? $data['form_ids'] : $delegation->form_ids,
'notify_delegator' => $data['notify_delegator'] ?? $delegation->notify_delegator,
'is_active' => $data['is_active'] ?? $delegation->is_active,
'reason' => $data['reason'] ?? $delegation->reason,
]);
$delegation->save();
return $delegation->fresh(['delegator:id,name', 'delegate:id,name']);
}
/**
* 위임 삭제
*/
public function delegationDestroy(int $id): bool
{
$tenantId = $this->tenantId();
$delegation = ApprovalDelegation::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
return $delegation->delete();
}
}