Files
sam-manage/app/Services/ApprovalService.php
김보곤 5fd69830ca feat: [approval] 기안함/완료함/대기함에 재상신 구분 열 추가
- resubmit_count 필드로 재상신 횟수 추적
- 반려 후 재상신 시 카운트 증가
- 보라색 뱃지로 재상신/재상신(N차) 표시
2026-03-05 13:06:58 +09:00

948 lines
33 KiB
PHP

<?php
namespace App\Services;
use App\Models\Approvals\Approval;
use App\Models\Approvals\ApprovalForm;
use App\Models\Approvals\ApprovalLine;
use App\Models\Approvals\ApprovalStep;
use App\Models\Boards\File;
use App\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
class ApprovalService
{
// =========================================================================
// 목록 조회
// =========================================================================
/**
* 기안함 (내가 기안한 문서)
*/
public function getMyDrafts(array $filters = [], int $perPage = 15): LengthAwarePaginator
{
$userId = auth()->id();
$query = Approval::with(['form', 'steps.approver'])
->byDrafter($userId);
$this->applyFilters($query, $filters);
return $query->orderByDesc('created_at')->paginate($perPage);
}
/**
* 결재 대기함 (내가 현재 결재자인 문서)
*/
public function getPendingForMe(int $userId, array $filters = [], int $perPage = 15): LengthAwarePaginator
{
$query = Approval::with(['form', 'drafter', 'steps.approver'])
->pending()
->whereHas('steps', function ($q) use ($userId) {
$q->where('approver_id', $userId)
->where('status', ApprovalStep::STATUS_PENDING)
->approvalOnly()
->where('step_order', function ($sub) {
$sub->selectRaw('MIN(step_order)')
->from('approval_steps as inner_steps')
->whereColumn('inner_steps.approval_id', 'approval_steps.approval_id')
->where('inner_steps.status', ApprovalStep::STATUS_PENDING)
->whereIn('inner_steps.step_type', [
ApprovalLine::STEP_TYPE_APPROVAL,
ApprovalLine::STEP_TYPE_AGREEMENT,
]);
});
});
$this->applyFilters($query, $filters);
return $query->orderByDesc('drafted_at')->paginate($perPage);
}
/**
* 처리 완료함 (내가 기안한 완료 문서 + 내가 결재 처리한 문서)
*/
public function getCompletedByMe(int $userId, array $filters = [], int $perPage = 15): LengthAwarePaginator
{
$query = Approval::with(['form', 'drafter', 'steps.approver'])
->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]);
});
});
$this->applyFilters($query, $filters);
return $query->orderByDesc('updated_at')->paginate($perPage);
}
/**
* 참조함 (내가 참조자인 문서)
*/
public function getReferencesForMe(int $userId, array $filters = [], int $perPage = 15): LengthAwarePaginator
{
$query = Approval::with(['form', 'drafter', 'steps.approver'])
->whereHas('steps', function ($q) use ($userId, $filters) {
$q->where('approver_id', $userId)
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE);
if (isset($filters['is_read'])) {
$q->where('is_read', $filters['is_read'] === 'true' || $filters['is_read'] === '1');
}
});
$this->applyFilters($query, $filters);
return $query->orderByDesc('created_at')->paginate($perPage);
}
// =========================================================================
// CRUD
// =========================================================================
/**
* 상세 조회
*/
public function getApproval(int $id): Approval
{
return Approval::with(['form', 'drafter', 'line', 'steps.approver'])
->findOrFail($id);
}
/**
* 생성 (임시저장)
*/
public function createApproval(array $data): Approval
{
return DB::transaction(function () use ($data) {
$tenantId = session('selected_tenant_id');
$userId = auth()->id();
$approval = Approval::create([
'tenant_id' => $tenantId,
'document_number' => $this->generateDocumentNumber($tenantId),
'form_id' => $data['form_id'],
'line_id' => $data['line_id'] ?? null,
'title' => $data['title'],
'content' => $data['content'] ?? [],
'body' => $data['body'] ?? null,
'status' => Approval::STATUS_DRAFT,
'is_urgent' => $data['is_urgent'] ?? false,
'drafter_id' => $userId,
'department_id' => $data['department_id'] ?? null,
'current_step' => 0,
'created_by' => $userId,
'updated_by' => $userId,
]);
if (! empty($data['steps'])) {
$this->saveApprovalSteps($approval, $data['steps']);
}
// 첨부파일 연결
if (! empty($data['attachment_file_ids'])) {
$this->linkAttachments($approval, $data['attachment_file_ids']);
}
return $approval->load(['form', 'drafter', 'steps.approver']);
});
}
/**
* 수정 (draft/rejected만)
*/
public function updateApproval(int $id, array $data): Approval
{
return DB::transaction(function () use ($id, $data) {
$approval = Approval::findOrFail($id);
if (! $approval->isEditable()) {
throw new \InvalidArgumentException('수정할 수 없는 상태입니다.');
}
$approval->update([
'form_id' => $data['form_id'] ?? $approval->form_id,
'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,
'updated_by' => auth()->id(),
]);
if (isset($data['steps'])) {
$approval->steps()->delete();
$this->saveApprovalSteps($approval, $data['steps']);
}
// 첨부파일 갱신
if (array_key_exists('attachment_file_ids', $data)) {
$this->linkAttachments($approval, $data['attachment_file_ids'] ?? []);
}
return $approval->load(['form', 'drafter', 'steps.approver']);
});
}
/**
* 삭제 (일반: draft만 / 관리자: 모든 상태)
*/
public function deleteApproval(int $id, ?User $user = null): bool
{
$approval = Approval::with('form')->findOrFail($id);
$user = $user ?? auth()->user();
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' => $user->id]);
return $approval->delete();
}
// =========================================================================
// 워크플로우
// =========================================================================
/**
* 상신 (draft/rejected → pending)
*/
public function submit(int $id): Approval
{
return DB::transaction(function () use ($id) {
$approval = Approval::with('steps')->findOrFail($id);
if (! $approval->isSubmittable()) {
throw new \InvalidArgumentException('상신할 수 없는 상태입니다.');
}
$approverSteps = $approval->steps
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT]);
if ($approverSteps->isEmpty()) {
throw new \InvalidArgumentException('결재선을 설정해주세요.');
}
// 반려 후 재상신이면 모든 step 초기화 + 재상신 카운트 증가
$isResubmit = $approval->status === Approval::STATUS_REJECTED;
if ($isResubmit) {
$approval->steps()->update([
'status' => ApprovalStep::STATUS_PENDING,
'comment' => null,
'acted_at' => null,
]);
}
$approval->update([
'status' => Approval::STATUS_PENDING,
'drafted_at' => now(),
'current_step' => 1,
'resubmit_count' => $isResubmit ? $approval->resubmit_count + 1 : $approval->resubmit_count,
'updated_by' => auth()->id(),
]);
return $approval->fresh(['form', 'drafter', 'steps.approver']);
});
}
/**
* 승인
*/
public function approve(int $id, ?string $comment = null): Approval
{
return DB::transaction(function () use ($id, $comment) {
$approval = Approval::with('steps')->findOrFail($id);
if (! $approval->isActionable()) {
throw new \InvalidArgumentException('승인할 수 없는 상태입니다.');
}
$currentStep = $approval->getCurrentApproverStep();
if (! $currentStep || $currentStep->approver_id !== auth()->id()) {
throw new \InvalidArgumentException('현재 결재자가 아닙니다.');
}
$currentStep->update([
'status' => ApprovalStep::STATUS_APPROVED,
'comment' => $comment,
'acted_at' => now(),
]);
// 다음 결재자 확인
$nextStep = $approval->steps()
->whereIn('step_type', [ApprovalLine::STEP_TYPE_APPROVAL, ApprovalLine::STEP_TYPE_AGREEMENT])
->where('status', ApprovalStep::STATUS_PENDING)
->orderBy('step_order')
->first();
if ($nextStep) {
$approval->update([
'current_step' => $nextStep->step_order,
'updated_by' => auth()->id(),
]);
} else {
// 마지막 결재자 → 문서 승인 완료
$approval->update([
'status' => Approval::STATUS_APPROVED,
'completed_at' => now(),
'drafter_read_at' => null,
'updated_by' => auth()->id(),
]);
// 연동 후처리 (휴가 등)
$this->handleApprovalCompleted($approval);
}
return $approval->fresh(['form', 'drafter', 'steps.approver']);
});
}
/**
* 반려
*/
public function reject(int $id, string $comment): Approval
{
return DB::transaction(function () use ($id, $comment) {
$approval = Approval::with('steps')->findOrFail($id);
if (! $approval->isActionable()) {
throw new \InvalidArgumentException('반려할 수 없는 상태입니다.');
}
$currentStep = $approval->getCurrentApproverStep();
if (! $currentStep || $currentStep->approver_id !== auth()->id()) {
throw new \InvalidArgumentException('현재 결재자가 아닙니다.');
}
if (empty($comment)) {
throw new \InvalidArgumentException('반려 사유를 입력해주세요.');
}
$currentStep->update([
'status' => ApprovalStep::STATUS_REJECTED,
'comment' => $comment,
'acted_at' => now(),
]);
$approval->update([
'status' => Approval::STATUS_REJECTED,
'completed_at' => now(),
'drafter_read_at' => null,
'updated_by' => auth()->id(),
]);
// 연동 후처리 (휴가 등)
$this->handleApprovalRejected($approval, $comment);
return $approval->fresh(['form', 'drafter', 'steps.approver']);
});
}
/**
* 회수 (기안자만, pending/on_hold → cancelled)
*/
public function cancel(int $id, ?string $recallReason = null): Approval
{
return DB::transaction(function () use ($id, $recallReason) {
$approval = Approval::with('steps')->findOrFail($id);
if (! $approval->isCancellable()) {
throw new \InvalidArgumentException('회수할 수 없는 상태입니다.');
}
if ($approval->drafter_id !== auth()->id()) {
throw new \InvalidArgumentException('기안자만 회수할 수 있습니다.');
}
// 첫 번째 결재자가 이미 처리했으면 회수 불가
$firstApproverStep = $approval->steps()
->approvalOnly()
->orderBy('step_order')
->first();
if ($firstApproverStep && $firstApproverStep->status !== ApprovalStep::STATUS_PENDING
&& $firstApproverStep->status !== ApprovalStep::STATUS_ON_HOLD) {
throw new \InvalidArgumentException('첫 번째 결재자가 이미 처리하여 회수할 수 없습니다.');
}
// 모든 pending/on_hold steps → skipped
$approval->steps()
->whereIn('status', [ApprovalStep::STATUS_PENDING, ApprovalStep::STATUS_ON_HOLD])
->update(['status' => ApprovalStep::STATUS_SKIPPED]);
$approval->update([
'status' => Approval::STATUS_CANCELLED,
'completed_at' => now(),
'recall_reason' => $recallReason,
'updated_by' => auth()->id(),
]);
// 연동 후처리 (휴가 회수)
$this->handleApprovalCancelled($approval);
return $approval->fresh(['form', 'drafter', 'steps.approver']);
});
}
/**
* 보류 (현재 결재자만, pending → on_hold)
*/
public function hold(int $id, string $comment): Approval
{
return DB::transaction(function () use ($id, $comment) {
$approval = Approval::with('steps')->findOrFail($id);
if (! $approval->isHoldable()) {
throw new \InvalidArgumentException('보류할 수 없는 상태입니다.');
}
$currentStep = $approval->getCurrentApproverStep();
if (! $currentStep || $currentStep->approver_id !== auth()->id()) {
throw new \InvalidArgumentException('현재 결재자가 아닙니다.');
}
if (empty($comment)) {
throw new \InvalidArgumentException('보류 사유를 입력해주세요.');
}
$currentStep->update([
'status' => ApprovalStep::STATUS_ON_HOLD,
'comment' => $comment,
'acted_at' => now(),
]);
$approval->update([
'status' => Approval::STATUS_ON_HOLD,
'updated_by' => auth()->id(),
]);
return $approval->fresh(['form', 'drafter', 'steps.approver']);
});
}
/**
* 보류 해제 (보류한 결재자만, on_hold → pending)
*/
public function releaseHold(int $id): Approval
{
return DB::transaction(function () use ($id) {
$approval = Approval::with('steps')->findOrFail($id);
if (! $approval->isHoldReleasable()) {
throw new \InvalidArgumentException('보류 해제할 수 없는 상태입니다.');
}
$holdStep = $approval->steps()
->where('status', ApprovalStep::STATUS_ON_HOLD)
->first();
if (! $holdStep || $holdStep->approver_id !== auth()->id()) {
throw new \InvalidArgumentException('보류한 결재자만 해제할 수 있습니다.');
}
$holdStep->update([
'status' => ApprovalStep::STATUS_PENDING,
'comment' => null,
'acted_at' => null,
]);
$approval->update([
'status' => Approval::STATUS_PENDING,
'updated_by' => auth()->id(),
]);
return $approval->fresh(['form', 'drafter', 'steps.approver']);
});
}
/**
* 전결 (현재 결재자가 이후 모든 결재를 건너뛰고 최종 승인)
*/
public function preDecide(int $id, ?string $comment = null): Approval
{
return DB::transaction(function () use ($id, $comment) {
$approval = Approval::with('steps')->findOrFail($id);
if (! $approval->isActionable()) {
throw new \InvalidArgumentException('전결할 수 없는 상태입니다.');
}
$currentStep = $approval->getCurrentApproverStep();
if (! $currentStep || $currentStep->approver_id !== auth()->id()) {
throw new \InvalidArgumentException('현재 결재자가 아닙니다.');
}
// 현재 step → approved + pre_decided
$currentStep->update([
'status' => ApprovalStep::STATUS_APPROVED,
'approval_type' => 'pre_decided',
'comment' => $comment,
'acted_at' => now(),
]);
// 이후 모든 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->update([
'status' => Approval::STATUS_APPROVED,
'completed_at' => now(),
'drafter_read_at' => null,
'updated_by' => auth()->id(),
]);
// 연동 후처리 (휴가 등)
$this->handleApprovalCompleted($approval);
return $approval->fresh(['form', 'drafter', 'steps.approver']);
});
}
/**
* 복사 재기안 (완료/반려/회수 문서를 복사하여 새 draft 생성)
*/
public function copyForRedraft(int $id): Approval
{
return DB::transaction(function () use ($id) {
$original = Approval::with('steps')->findOrFail($id);
if (! $original->isCopyable()) {
throw new \InvalidArgumentException('복사할 수 없는 상태입니다.');
}
if ($original->drafter_id !== auth()->id()) {
throw new \InvalidArgumentException('기안자만 복사할 수 있습니다.');
}
$tenantId = session('selected_tenant_id');
// 새 문서 생성
$newApproval = Approval::create([
'tenant_id' => $tenantId,
'document_number' => $this->generateDocumentNumber($tenantId),
'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' => auth()->id(),
'department_id' => $original->department_id,
'current_step' => 0,
'parent_doc_id' => $original->id,
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
// 결재선 복사 (모두 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->load(['form', 'drafter', 'steps.approver']);
});
}
/**
* 참조 열람 추적
*/
public function markAsRead(int $approvalId): void
{
$userId = auth()->id();
ApprovalStep::where('approval_id', $approvalId)
->where('approver_id', $userId)
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE)
->where('is_read', false)
->update([
'is_read' => true,
'read_at' => now(),
]);
}
// =========================================================================
// 결재선
// =========================================================================
/**
* 결재선 템플릿 목록
*/
public function getApprovalLines(): Collection
{
return ApprovalLine::orderBy('name')->get();
}
/**
* 결재선 템플릿 생성
*/
public function createLine(array $data): ApprovalLine
{
$tenantId = session('selected_tenant_id');
$steps = $this->enrichLineSteps($data['steps']);
return ApprovalLine::create([
'tenant_id' => $tenantId,
'name' => $data['name'],
'steps' => $steps,
'is_default' => $data['is_default'] ?? false,
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
}
/**
* 결재선 템플릿 수정
*/
public function updateLine(int $id, array $data): ApprovalLine
{
$line = ApprovalLine::findOrFail($id);
$steps = $this->enrichLineSteps($data['steps']);
$line->update([
'name' => $data['name'],
'steps' => $steps,
'is_default' => $data['is_default'] ?? false,
'updated_by' => auth()->id(),
]);
return $line->fresh();
}
/**
* 결재선 템플릿 삭제
*/
public function deleteLine(int $id): bool
{
$line = ApprovalLine::findOrFail($id);
$line->update(['deleted_by' => auth()->id()]);
return $line->delete();
}
/**
* 양식 목록
*/
public function getApprovalForms(): Collection
{
return ApprovalForm::active()->orderBy('name')->get();
}
/**
* 결재 단계 저장 + 스냅샷
*/
public function saveApprovalSteps(Approval $approval, array $steps): void
{
foreach ($steps as $index => $step) {
$user = User::find($step['user_id']);
$tenantId = session('selected_tenant_id');
$departmentName = null;
$positionName = null;
if ($user) {
$employee = $user->tenantProfiles()
->where('tenant_id', $tenantId)
->first();
if ($employee) {
$departmentName = $employee->department?->name;
$positionName = $employee->position_label ?? $employee->job_title_label;
}
}
ApprovalStep::create([
'approval_id' => $approval->id,
'step_order' => $index + 1,
'step_type' => $step['step_type'] ?? ApprovalLine::STEP_TYPE_APPROVAL,
'approver_id' => $step['user_id'],
'approver_name' => $user?->name ?? '',
'approver_department' => $departmentName,
'approver_position' => $positionName,
'status' => ApprovalStep::STATUS_PENDING,
]);
}
}
/**
* 첨부파일 연결
*/
public function linkAttachments(Approval $approval, array $fileIds): void
{
$attachments = [];
if (! empty($fileIds)) {
$files = File::whereIn('id', $fileIds)
->where('uploaded_by', auth()->id())
->get();
foreach ($files as $file) {
$attachments[] = [
'id' => $file->id,
'name' => $file->original_name,
'size' => $file->file_size,
'mime_type' => $file->mime_type,
];
$file->update([
'is_temp' => false,
'document_id' => $approval->id,
'document_type' => 'approval',
]);
}
}
$approval->update(['attachments' => $attachments]);
}
/**
* 미처리 건수 (뱃지용)
*/
public function getBadgeCounts(int $userId): array
{
$pendingCount = Approval::pending()
->whereHas('steps', function ($q) use ($userId) {
$q->where('approver_id', $userId)
->where('status', ApprovalStep::STATUS_PENDING)
->approvalOnly()
->where('step_order', function ($sub) {
$sub->selectRaw('MIN(step_order)')
->from('approval_steps as inner_steps')
->whereColumn('inner_steps.approval_id', 'approval_steps.approval_id')
->where('inner_steps.status', ApprovalStep::STATUS_PENDING)
->whereIn('inner_steps.step_type', [
ApprovalLine::STEP_TYPE_APPROVAL,
ApprovalLine::STEP_TYPE_AGREEMENT,
]);
});
})
->count();
$draftCount = Approval::pending()->byDrafter($userId)->count();
$referenceUnreadCount = ApprovalStep::where('approver_id', $userId)
->where('step_type', ApprovalLine::STEP_TYPE_REFERENCE)
->where('is_read', false)
->count();
$completedUnreadCount = Approval::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 $userId): int
{
return Approval::where('drafter_id', $userId)
->whereIn('status', [Approval::STATUS_APPROVED, Approval::STATUS_REJECTED])
->whereNull('drafter_read_at')
->update(['drafter_read_at' => now()]);
}
// =========================================================================
// Private 헬퍼
// =========================================================================
/**
* 휴가/근태신청/사유서 관련 결재 양식인지 확인
*/
private function isLeaveRelatedForm(?string $code): bool
{
return in_array($code, ['leave', 'attendance_request', 'reason_report']);
}
/**
* 결재 최종 승인 시 연동 처리 (휴가/근태신청/사유서)
*/
private function handleApprovalCompleted(Approval $approval): void
{
if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) {
return;
}
$leave = \App\Models\HR\Leave::where('approval_id', $approval->id)->first();
if ($leave && $leave->status === 'pending') {
app(\App\Services\HR\LeaveService::class)->approveByApproval($leave, $approval);
}
}
/**
* 결재 반려 시 연동 처리 (휴가/근태신청/사유서)
*/
private function handleApprovalRejected(Approval $approval, string $comment): void
{
if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) {
return;
}
$leave = \App\Models\HR\Leave::where('approval_id', $approval->id)->first();
if ($leave && $leave->status === 'pending') {
app(\App\Services\HR\LeaveService::class)->rejectByApproval(
$leave,
$comment,
auth()->id()
);
}
}
/**
* 결재 삭제 시 연동 처리 (휴가/근태신청/사유서)
*/
private function handleApprovalDeleted(Approval $approval): void
{
if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) {
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(),
]);
}
}
/**
* 결재 회수 시 연동 처리 (휴가/근태신청/사유서)
*/
private function handleApprovalCancelled(Approval $approval): void
{
if (! $approval->form || ! $this->isLeaveRelatedForm($approval->form->code)) {
return;
}
$leave = \App\Models\HR\Leave::where('approval_id', $approval->id)->first();
if ($leave && $leave->status === 'pending') {
$leave->update([
'status' => 'cancelled',
'updated_by' => auth()->id(),
]);
}
}
/**
* 결재선 steps에 user_name, department, position 스냅샷 보강
*/
private function enrichLineSteps(array $steps): array
{
$tenantId = session('selected_tenant_id');
return collect($steps)->map(function ($step) use ($tenantId) {
$user = User::find($step['user_id']);
$profile = $user?->tenantProfiles()->where('tenant_id', $tenantId)->first();
return [
'user_id' => $step['user_id'],
'user_name' => $user?->name ?? '',
'department' => $profile?->department?->name ?? $step['department'] ?? '',
'position' => $profile?->position_label ?? $profile?->job_title_label ?? $step['position'] ?? '',
'step_type' => $step['step_type'] ?? 'approval',
];
})->toArray();
}
/**
* 문서번호 채번 (APR-YYMMDD-001)
*/
private function generateDocumentNumber(int $tenantId): string
{
$prefix = 'APR';
$dateKey = now()->format('ymd');
$documentType = 'approval';
$periodKey = $dateKey;
DB::statement(
'INSERT INTO numbering_sequences
(tenant_id, document_type, scope_key, period_key, last_sequence, created_at, updated_at)
VALUES (?, ?, ?, ?, 1, NOW(), NOW())
ON DUPLICATE KEY UPDATE
last_sequence = last_sequence + 1,
updated_at = NOW()',
[$tenantId, $documentType, '', $periodKey]
);
$sequence = (int) DB::table('numbering_sequences')
->where('tenant_id', $tenantId)
->where('document_type', $documentType)
->where('scope_key', '')
->where('period_key', $periodKey)
->value('last_sequence');
return $prefix.'-'.$dateKey.'-'.str_pad((string) $sequence, 3, '0', STR_PAD_LEFT);
}
/**
* 공통 필터 적용
*/
private function applyFilters($query, array $filters): void
{
if (! empty($filters['search'])) {
$search = $filters['search'];
$query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('document_number', 'like', "%{$search}%");
});
}
if (! empty($filters['status'])) {
$query->where('status', $filters['status']);
}
if (! empty($filters['is_urgent'])) {
$query->where('is_urgent', true);
}
if (! empty($filters['date_from'])) {
$query->where('created_at', '>=', $filters['date_from']);
}
if (! empty($filters['date_to'])) {
$query->where('created_at', '<=', $filters['date_to'].' 23:59:59');
}
}
}