tenantId(); $query = ApprovalForm::query() ->where('tenant_id', $tenantId) ->with('creator:id,name'); // 카테고리 필터 if (! empty($params['category'])) { $query->where('category', $params['category']); } // 활성 상태 필터 if (isset($params['is_active'])) { $query->where('is_active', $params['is_active']); } // 검색 if (! empty($params['search'])) { $query->where(function ($q) use ($params) { $q->where('name', 'like', "%{$params['search']}%") ->orWhere('code', 'like', "%{$params['search']}%"); }); } // 정렬 $sortBy = $params['sort_by'] ?? 'created_at'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 활성 결재 양식 목록 (셀렉트박스용) */ public function formActive(): Collection { $tenantId = $this->tenantId(); return ApprovalForm::query() ->where('tenant_id', $tenantId) ->active() ->orderBy('name') ->get(['id', 'name', 'code', 'category']); } /** * 결재 양식 상세 */ public function formShow(int $id): ApprovalForm { $tenantId = $this->tenantId(); return ApprovalForm::query() ->where('tenant_id', $tenantId) ->with('creator:id,name') ->findOrFail($id); } /** * 결재 양식 생성 */ public function formStore(array $data): ApprovalForm { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 코드 중복 확인 $exists = ApprovalForm::query() ->where('tenant_id', $tenantId) ->where('code', $data['code']) ->exists(); if ($exists) { throw new BadRequestHttpException(__('error.approval.form_code_exists')); } return ApprovalForm::create([ 'tenant_id' => $tenantId, 'name' => $data['name'], 'code' => $data['code'], 'category' => $data['category'] ?? null, 'template' => $data['template'], 'is_active' => $data['is_active'] ?? true, 'created_by' => $userId, 'updated_by' => $userId, ]); } /** * 결재 양식 수정 */ public function formUpdate(int $id, array $data): ApprovalForm { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $form = ApprovalForm::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 코드 중복 확인 (자기 자신 제외) if (isset($data['code']) && $data['code'] !== $form->code) { $exists = ApprovalForm::query() ->where('tenant_id', $tenantId) ->where('code', $data['code']) ->where('id', '!=', $id) ->exists(); if ($exists) { throw new BadRequestHttpException(__('error.approval.form_code_exists')); } } $form->fill([ 'name' => $data['name'] ?? $form->name, 'code' => $data['code'] ?? $form->code, 'category' => $data['category'] ?? $form->category, 'template' => $data['template'] ?? $form->template, 'is_active' => $data['is_active'] ?? $form->is_active, 'updated_by' => $userId, ]); $form->save(); return $form->fresh('creator:id,name'); } /** * 결재 양식 삭제 */ public function formDestroy(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $form = ApprovalForm::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 사용 중인 양식인지 확인 $inUse = Approval::query() ->where('form_id', $id) ->exists(); if ($inUse) { throw new BadRequestHttpException(__('error.approval.form_in_use')); } $form->deleted_by = $userId; $form->save(); $form->delete(); return true; } // ========================================================================= // 결재선 템플릿 관리 // ========================================================================= /** * 결재선 목록 */ public function lineIndex(array $params): LengthAwarePaginator { $tenantId = $this->tenantId(); $query = ApprovalLine::query() ->where('tenant_id', $tenantId) ->with('creator:id,name'); // 검색 if (! empty($params['search'])) { $query->where('name', 'like', "%{$params['search']}%"); } // 정렬 $sortBy = $params['sort_by'] ?? 'created_at'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 결재선 상세 */ public function lineShow(int $id): ApprovalLine { $tenantId = $this->tenantId(); return ApprovalLine::query() ->where('tenant_id', $tenantId) ->with('creator:id,name') ->findOrFail($id); } /** * 결재선 생성 */ public function lineStore(array $data): ApprovalLine { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { // 기본 결재선으로 설정 시 기존 기본값 해제 if (! empty($data['is_default'])) { ApprovalLine::query() ->where('tenant_id', $tenantId) ->where('is_default', true) ->update(['is_default' => false]); } return ApprovalLine::create([ 'tenant_id' => $tenantId, 'name' => $data['name'], 'steps' => $data['steps'], 'is_default' => $data['is_default'] ?? false, 'created_by' => $userId, 'updated_by' => $userId, ]); }); } /** * 결재선 수정 */ public function lineUpdate(int $id, array $data): ApprovalLine { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $data, $tenantId, $userId) { $line = ApprovalLine::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 기본 결재선으로 설정 시 기존 기본값 해제 if (! empty($data['is_default']) && ! $line->is_default) { ApprovalLine::query() ->where('tenant_id', $tenantId) ->where('is_default', true) ->update(['is_default' => false]); } $line->fill([ 'name' => $data['name'] ?? $line->name, 'steps' => $data['steps'] ?? $line->steps, 'is_default' => $data['is_default'] ?? $line->is_default, 'updated_by' => $userId, ]); $line->save(); return $line->fresh('creator:id,name'); }); } /** * 결재선 삭제 */ public function lineDestroy(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $line = ApprovalLine::query() ->where('tenant_id', $tenantId) ->findOrFail($id); $line->deleted_by = $userId; $line->save(); $line->delete(); return true; } // ========================================================================= // 결재 문서 관리 // ========================================================================= /** * 기안함 - 내가 기안한 문서 */ public function drafts(array $params): LengthAwarePaginator { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $query = Approval::query() ->where('tenant_id', $tenantId) ->where('drafter_id', $userId) ->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', 'steps.approver.tenantProfile:id,user_id,position_key,department_id', 'steps.approver.tenantProfile.department:id,name', ]); // 상태 필터 if (! empty($params['status'])) { $query->where('status', $params['status']); } // 검색 if (! empty($params['search'])) { $query->where(function ($q) use ($params) { $q->where('title', 'like', "%{$params['search']}%") ->orWhere('document_number', 'like', "%{$params['search']}%"); }); } // 정렬 $sortBy = $params['sort_by'] ?? 'created_at'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 기안함 현황 카드 */ public function draftsSummary(): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $counts = Approval::query() ->where('tenant_id', $tenantId) ->where('drafter_id', $userId) ->selectRaw('status, COUNT(*) as count') ->groupBy('status') ->pluck('count', 'status') ->toArray(); return [ 'total' => array_sum($counts), 'draft' => $counts[Approval::STATUS_DRAFT] ?? 0, 'pending' => $counts[Approval::STATUS_PENDING] ?? 0, 'approved' => $counts[Approval::STATUS_APPROVED] ?? 0, 'rejected' => $counts[Approval::STATUS_REJECTED] ?? 0, ]; } /** * 결재함 - 내가 결재해야 할 문서 */ public function inbox(array $params): LengthAwarePaginator { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $query = Approval::query() ->where('tenant_id', $tenantId) ->whereHas('steps', function ($q) use ($userId) { $q->where('approver_id', $userId) ->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]); }) ->with([ 'form:id,name,code,category', 'drafter:id,name', 'drafter.tenantProfile:id,user_id,position_key,department_id', 'drafter.tenantProfile.department:id,name', 'steps' => function ($q) use ($userId) { $q->where('approver_id', $userId); }, 'steps.approver:id,name', 'steps.approver.tenantProfile:id,user_id,position_key,department_id', 'steps.approver.tenantProfile.department:id,name', ]); // 상태 필터 if (! empty($params['status'])) { if ($params['status'] === 'requested') { // 결재 요청 (현재 내 차례) $query->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]); }); } elseif ($params['status'] === 'scheduled') { // 예정 (아직 내 차례 아님) $query->where('status', Approval::STATUS_PENDING) ->whereHas('steps', function ($q) use ($userId) { $q->where('approver_id', $userId) ->where('status', ApprovalStep::STATUS_PENDING); }) ->whereDoesntHave('steps', function ($q) use ($userId) { $q->where('approver_id', $userId) ->where('status', ApprovalStep::STATUS_PENDING) ->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]); }); } elseif ($params['status'] === 'completed') { // 내가 처리 완료한 문서 $query->whereHas('steps', function ($q) use ($userId) { $q->where('approver_id', $userId) ->whereIn('status', [ApprovalStep::STATUS_APPROVED, ApprovalStep::STATUS_REJECTED]); }); } elseif ($params['status'] === 'rejected') { // 내가 반려한 문서 $query->whereHas('steps', function ($q) use ($userId) { $q->where('approver_id', $userId) ->where('status', ApprovalStep::STATUS_REJECTED); }); } } // 날짜 범위 필터 if (! empty($params['start_date'])) { $query->whereDate('created_at', '>=', $params['start_date']); } if (! empty($params['end_date'])) { $query->whereDate('created_at', '<=', $params['end_date']); } // 정렬 $sortBy = $params['sort_by'] ?? 'created_at'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 결재함 현황 카드 */ public function inboxSummary(): array { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); // 결재 요청 (현재 내 차례) $requested = 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(); // 예정 (내 차례 대기중) $scheduled = ApprovalStep::query() ->where('approver_id', $userId) ->where('status', ApprovalStep::STATUS_PENDING) ->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]) ->whereHas('approval', function ($q) use ($tenantId) { $q->where('tenant_id', $tenantId) ->where('status', Approval::STATUS_PENDING); }) ->count() - $requested; // 완료 (내가 처리한 문서) $completed = ApprovalStep::query() ->where('approver_id', $userId) ->whereIn('status', [ApprovalStep::STATUS_APPROVED, ApprovalStep::STATUS_REJECTED]) ->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]) ->whereHas('approval', function ($q) use ($tenantId) { $q->where('tenant_id', $tenantId); }) ->count(); // 반려 (내가 반려한 문서) $rejected = ApprovalStep::query() ->where('approver_id', $userId) ->where('status', ApprovalStep::STATUS_REJECTED) ->whereHas('approval', function ($q) use ($tenantId) { $q->where('tenant_id', $tenantId); }) ->count(); return [ 'requested' => max(0, $requested), 'scheduled' => max(0, $scheduled), 'completed' => $completed, 'rejected' => $rejected, ]; } /** * 참조함 - 내가 참조된 문서 */ public function reference(array $params): LengthAwarePaginator { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $query = Approval::query() ->where('tenant_id', $tenantId) ->whereHas('steps', function ($q) use ($userId) { $q->where('approver_id', $userId) ->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE); }) ->with(['form:id,name,code,category', 'drafter:id,name', 'steps' => function ($q) use ($userId) { $q->where('approver_id', $userId) ->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE); }]); // 열람 상태 필터 if (isset($params['is_read'])) { $query->whereHas('steps', function ($q) use ($userId, $params) { $q->where('approver_id', $userId) ->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE) ->where('is_read', $params['is_read']); }); } // 정렬 $sortBy = $params['sort_by'] ?? 'created_at'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 결재 문서 상세 */ public function show(int $id): Approval { $tenantId = $this->tenantId(); $approval = Approval::query() ->where('tenant_id', $tenantId) ->with([ 'form:id,name,code,category,template', 'drafter:id,name,email', 'drafter.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name', 'drafter.tenantProfile.department:id,name', 'steps.approver:id,name,email', 'steps.approver.tenantProfile:id,user_id,tenant_id,department_id,position_key,display_name', 'steps.approver.tenantProfile.department:id,name', ]) ->findOrFail($id); // Document 브릿지: 연결된 문서 데이터 로딩 if ($approval->linkable_type === Document::class) { $approval->load([ 'linkable.template', 'linkable.template.approvalLines', 'linkable.data', 'linkable.approvals.user:id,name', 'linkable.attachments', ]); } return $approval; } /** * 결재 문서 생성 (임시저장 또는 상신) */ public function store(array $data): Approval { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { // 양식 확인 (form_id 또는 form_code 지원) $formQuery = ApprovalForm::query() ->where('tenant_id', $tenantId) ->active(); if (! empty($data['form_id'])) { $form = $formQuery->where('id', $data['form_id'])->firstOrFail(); } elseif (! empty($data['form_code'])) { $form = $formQuery->where('code', $data['form_code'])->firstOrFail(); } else { throw new BadRequestHttpException(__('error.approval.form_required')); } // 문서번호 생성 $documentNumber = $this->generateDocumentNumber($tenantId); $status = ! empty($data['submit']) ? Approval::STATUS_PENDING : Approval::STATUS_DRAFT; $approval = Approval::create([ '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, 'updated_by' => $userId, ]); // 결재선 생성 (steps가 있으면 항상 저장) if (! empty($data['steps'])) { $this->createApprovalSteps($approval, $data['steps']); } 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', ]); }); } /** * 결재 문서 수정 (임시저장 상태만) */ public function update(int $id, array $data): Approval { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $data, $tenantId, $userId) { $approval = Approval::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $approval->isEditable()) { throw new BadRequestHttpException(__('error.approval.not_editable')); } // form_id 또는 form_code로 양식 ID 결정 $formId = $approval->form_id; if (! empty($data['form_id'])) { $formId = $data['form_id']; } elseif (! empty($data['form_code'])) { $form = ApprovalForm::query() ->where('tenant_id', $tenantId) ->where('code', $data['form_code']) ->active() ->first(); if ($form) { $formId = $form->id; } } $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, ]); $approval->save(); // 결재선 수정 (steps가 전달된 경우) if (! empty($data['steps'])) { // 기존 결재선 삭제 후 새로 생성 $approval->steps()->delete(); $this->createApprovalSteps($approval, $data['steps']); } 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', ]); }); } /** * 결재 문서 삭제 (임시저장 상태만) */ public function destroy(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $approval = Approval::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $approval->isDeletable()) { throw new BadRequestHttpException(__('error.approval.not_deletable')); } $approval->deleted_by = $userId; $approval->save(); $approval->delete(); return true; }); } /** * 결재 상신 (draft → pending, rejected → pending 재상신 포함) */ public function submit(int $id, array $data): Approval { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $data, $tenantId, $userId) { $approval = Approval::query() ->where('tenant_id', $tenantId) ->with('steps') ->findOrFail($id); if (! $approval->isSubmittable()) { throw new BadRequestHttpException(__('error.approval.not_submittable')); } // 기존 결재선 확인 (steps 없이 상신하는 경우) if (empty($data['steps'])) { $existingSteps = $approval->steps() ->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]) ->count(); if ($existingSteps === 0) { throw new BadRequestHttpException(__('error.approval.steps_required')); } } // 반려 후 재상신 처리 $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가 있으면 새로 생성 if (! empty($data['steps'])) { $approval->steps()->delete(); $this->createApprovalSteps($approval, $data['steps']); } else { // 기존 결재선 사용 시 알림 발송 $firstPendingStep = $approval->steps() ->where('status', ApprovalStep::STATUS_PENDING) ->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]) ->orderBy('step_order') ->first(); if ($firstPendingStep) { $this->todayIssueService->handleApprovalStepChange($firstPendingStep); } } 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', ]); }); } /** * 결재 승인 */ public function approve(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) ->findOrFail($id); if (! $approval->isActionable()) { throw new BadRequestHttpException(__('error.approval.not_actionable')); } // 현재 내 결재 단계 찾기 $myStep = $approval->steps() ->where('approver_id', $userId) ->where('status', ApprovalStep::STATUS_PENDING) ->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]) ->orderBy('step_order') ->first(); if (! $myStep) { throw new BadRequestHttpException(__('error.approval.not_your_turn')); } // 내 단계 승인 $myStep->status = ApprovalStep::STATUS_APPROVED; $myStep->comment = $comment; $myStep->acted_at = now(); $myStep->save(); // 다음 결재자 확인 $nextStep = $approval->steps() ->where('status', ApprovalStep::STATUS_PENDING) ->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]) ->orderBy('step_order') ->first(); if (! $nextStep) { // 모든 결재 완료 $approval->status = Approval::STATUS_APPROVED; $approval->completed_at = now(); $approval->drafter_read_at = null; } $approval->current_step = $myStep->step_order + 1; $approval->updated_by = $userId; $approval->save(); // 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', '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', ]); }); } /** * 결재 반려 */ public function reject(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) ->findOrFail($id); if (! $approval->isActionable()) { throw new BadRequestHttpException(__('error.approval.not_actionable')); } // 현재 내 결재 단계 찾기 $myStep = $approval->steps() ->where('approver_id', $userId) ->where('status', ApprovalStep::STATUS_PENDING) ->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]) ->orderBy('step_order') ->first(); if (! $myStep) { throw new BadRequestHttpException(__('error.approval.not_your_turn')); } // 반려 처리 $myStep->status = ApprovalStep::STATUS_REJECTED; $myStep->comment = $comment; $myStep->acted_at = now(); $myStep->save(); // 문서 반려 상태로 변경 $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', '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', ]); }); } /** * 결재 회수 (기안자만, pending/on_hold → cancelled) */ public function cancel(int $id, ?string $recallReason = null): Approval { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $recallReason, $tenantId, $userId) { $approval = Approval::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $approval->isCancellable()) { throw new BadRequestHttpException(__('error.approval.not_cancellable')); } // 기안자만 회수 가능 if ($approval->drafter_id !== $userId) { 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 브릿지 동기화 $this->syncToLinkedDocument($approval); // 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의 상태와 결재란을 동기화 */ private function syncToLinkedDocument(Approval $approval): void { if ($approval->linkable_type !== Document::class) { return; } $document = Document::find($approval->linkable_id); if (! $document) { return; } // approval_steps → document_approvals 동기화 (승인자 이름/시각 반영) foreach ($approval->steps as $step) { if ($step->status === ApprovalStep::STATUS_PENDING) { continue; } $docApproval = $document->approvals() ->where('step', $step->step_order) ->first(); if ($docApproval) { $docApproval->update([ 'status' => strtoupper($step->status), 'acted_at' => $step->acted_at, 'comment' => $step->comment, ]); } } // Document 전체 상태 동기화 $documentStatus = match ($approval->status) { Approval::STATUS_APPROVED => Document::STATUS_APPROVED, Approval::STATUS_REJECTED => Document::STATUS_REJECTED, Approval::STATUS_CANCELLED => Document::STATUS_CANCELLED, default => Document::STATUS_PENDING, }; $document->update([ 'status' => $documentStatus, 'completed_at' => in_array($approval->status, [ Approval::STATUS_APPROVED, Approval::STATUS_REJECTED, ]) ? now() : null, ]); } /** * 참조 열람 처리 */ public function markRead(int $id): Approval { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $approval = Approval::query() ->where('tenant_id', $tenantId) ->findOrFail($id); $step = $approval->steps() ->where('approver_id', $userId) ->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE) ->first(); if (! $step) { throw new NotFoundHttpException(__('error.approval.not_referee')); } $step->is_read = true; $step->read_at = now(); $step->save(); 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', ]); } /** * 참조 미열람 처리 */ public function markUnread(int $id): Approval { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $approval = Approval::query() ->where('tenant_id', $tenantId) ->findOrFail($id); $step = $approval->steps() ->where('approver_id', $userId) ->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE) ->first(); if (! $step) { throw new NotFoundHttpException(__('error.approval.not_referee')); } $step->is_read = false; $step->read_at = null; $step->save(); 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', ]); } // ========================================================================= // 헬퍼 메서드 // ========================================================================= /** * 문서번호 생성 */ private function generateDocumentNumber(int $tenantId): string { $prefix = 'AP'; $date = now()->format('Ymd'); $lastNumber = Approval::query() ->where('tenant_id', $tenantId) ->where('document_number', 'like', "{$prefix}-{$date}-%") ->orderByDesc('document_number') ->value('document_number'); if ($lastNumber) { $sequence = (int) substr($lastNumber, -4) + 1; } else { $sequence = 1; } return sprintf('%s-%s-%04d', $prefix, $date, $sequence); } /** * 결재 단계 생성 + 결재자 스냅샷 저장 * 프론트엔드 호환성: step_type/approver_id 또는 type/user_id 지원 * 중복 결재자 자동 제거 */ private function createApprovalSteps(Approval $approval, array $steps): void { $tenantId = $this->tenantId(); $order = 1; $processedApprovers = []; // 중복 체크용 foreach ($steps as $step) { // 필드명 호환성: step_type 또는 type $stepType = $step['step_type'] ?? $step['type'] ?? null; // 필드명 호환성: approver_id 또는 user_id $approverId = $step['approver_id'] ?? $step['user_id'] ?? null; // step_order가 있으면 사용, 없으면 자동 증가 $stepOrder = $step['step_order'] ?? $order++; if ($stepType && $approverId) { // 동일 결재자 중복 건너뛰기 if (in_array($approverId, $processedApprovers, true)) { continue; } $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(); } }