tenantId(); $query = Leave::query() ->where('tenant_id', $tenantId) ->with([ 'user:id,name,email', 'userProfile' => fn ($q) => $q->where('tenant_id', $tenantId), 'userProfile.department:id,name', 'approver:id,name', ]); // 사용자 필터 if (! empty($params['user_id'])) { $query->where('user_id', $params['user_id']); } // 상태 필터 if (! empty($params['status'])) { $query->where('status', $params['status']); } // 휴가 유형 필터 if (! empty($params['leave_type'])) { $query->where('leave_type', $params['leave_type']); } // 날짜 범위 필터 if (! empty($params['date_from'])) { $query->where('start_date', '>=', $params['date_from']); } if (! empty($params['date_to'])) { $query->where('end_date', '<=', $params['date_to']); } // 연도 필터 if (! empty($params['year'])) { $query->whereYear('start_date', $params['year']); } // 부서 필터 if (! empty($params['department_id'])) { $query->whereHas('user.tenantProfile', function ($q) use ($params) { $q->where('department_id', $params['department_id']); }); } // 정렬 $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): Leave { $tenantId = $this->tenantId(); return Leave::query() ->where('tenant_id', $tenantId) ->with([ 'user:id,name,email', 'userProfile' => fn ($q) => $q->where('tenant_id', $tenantId), 'userProfile.department:id,name', 'approver:id,name', ]) ->findOrFail($id); } /** * 휴가 신청 */ public function store(array $data): Leave { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { // 신청자 ID (관리자가 대리 신청 가능) $applicantId = $data['user_id'] ?? $userId; // 잔여 휴가 확인 (연차/반차만) if (in_array($data['leave_type'], [Leave::TYPE_ANNUAL, Leave::TYPE_HALF_AM, Leave::TYPE_HALF_PM])) { $year = \Carbon\Carbon::parse($data['start_date'])->year; $balance = $this->getOrCreateBalance($tenantId, $applicantId, $year); if (! $balance->canUse($data['days'])) { throw new BadRequestHttpException(__('error.leave.insufficient_balance')); } } // 중복 휴가 확인 $overlapping = Leave::query() ->where('tenant_id', $tenantId) ->where('user_id', $applicantId) ->whereIn('status', [Leave::STATUS_PENDING, Leave::STATUS_APPROVED]) ->where(function ($q) use ($data) { $q->whereBetween('start_date', [$data['start_date'], $data['end_date']]) ->orWhereBetween('end_date', [$data['start_date'], $data['end_date']]) ->orWhere(function ($q2) use ($data) { $q2->where('start_date', '<=', $data['start_date']) ->where('end_date', '>=', $data['end_date']); }); }) ->exists(); if ($overlapping) { throw new BadRequestHttpException(__('error.leave.overlapping')); } $leave = Leave::create([ 'tenant_id' => $tenantId, 'user_id' => $applicantId, 'leave_type' => $data['leave_type'], 'start_date' => $data['start_date'], 'end_date' => $data['end_date'], 'days' => $data['days'], 'reason' => $data['reason'] ?? null, 'status' => Leave::STATUS_PENDING, 'created_by' => $userId, 'updated_by' => $userId, ]); return $leave->fresh(['user:id,name,email']); }); } /** * 휴가 수정 (pending 상태만) */ public function update(int $id, array $data): Leave { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $data, $tenantId, $userId) { $leave = Leave::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $leave->isEditable()) { throw new BadRequestHttpException(__('error.leave.not_editable')); } // 휴가 기간이 변경된 경우 잔여 휴가 확인 if (isset($data['days']) && in_array($leave->leave_type, [Leave::TYPE_ANNUAL, Leave::TYPE_HALF_AM, Leave::TYPE_HALF_PM])) { $year = \Carbon\Carbon::parse($data['start_date'] ?? $leave->start_date)->year; $balance = $this->getOrCreateBalance($tenantId, $leave->user_id, $year); if (! $balance->canUse($data['days'])) { throw new BadRequestHttpException(__('error.leave.insufficient_balance')); } } $leave->fill([ 'leave_type' => $data['leave_type'] ?? $leave->leave_type, 'start_date' => $data['start_date'] ?? $leave->start_date, 'end_date' => $data['end_date'] ?? $leave->end_date, 'days' => $data['days'] ?? $leave->days, 'reason' => $data['reason'] ?? $leave->reason, 'updated_by' => $userId, ]); $leave->save(); return $leave->fresh(['user:id,name,email']); }); } /** * 휴가 취소/삭제 (pending 상태만) */ public function destroy(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $leave = Leave::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $leave->isEditable()) { throw new BadRequestHttpException(__('error.leave.not_editable')); } $leave->deleted_by = $userId; $leave->save(); $leave->delete(); return true; }); } /** * 휴가 승인 */ public function approve(int $id, ?string $comment = null): Leave { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $leave = Leave::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $leave->isApprovable()) { throw new BadRequestHttpException(__('error.leave.not_approvable')); } // 잔여 휴가 차감 (연차/반차만) if (in_array($leave->leave_type, [Leave::TYPE_ANNUAL, Leave::TYPE_HALF_AM, Leave::TYPE_HALF_PM])) { $year = \Carbon\Carbon::parse($leave->start_date)->year; $balance = $this->getOrCreateBalance($tenantId, $leave->user_id, $year); if (! $balance->canUse($leave->days)) { throw new BadRequestHttpException(__('error.leave.insufficient_balance')); } $balance->useLeave($leave->days); } $leave->status = Leave::STATUS_APPROVED; $leave->approved_by = $userId; $leave->approved_at = now(); $leave->updated_by = $userId; $leave->save(); return $leave->fresh(['user:id,name,email', 'approver:id,name']); }); } /** * 휴가 반려 */ public function reject(int $id, string $reason): Leave { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $reason, $tenantId, $userId) { $leave = Leave::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $leave->isApprovable()) { throw new BadRequestHttpException(__('error.leave.not_approvable')); } $leave->status = Leave::STATUS_REJECTED; $leave->approved_by = $userId; $leave->approved_at = now(); $leave->reject_reason = $reason; $leave->updated_by = $userId; $leave->save(); return $leave->fresh(['user:id,name,email', 'approver:id,name']); }); } /** * 승인된 휴가 취소 (휴가 복원) */ public function cancel(int $id, ?string $reason = null): Leave { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $reason, $tenantId, $userId) { $leave = Leave::query() ->where('tenant_id', $tenantId) ->findOrFail($id); if (! $leave->isCancellable()) { throw new BadRequestHttpException(__('error.leave.not_cancellable')); } // 이미 승인된 휴가라면 잔여일수 복원 if ($leave->status === Leave::STATUS_APPROVED) { if (in_array($leave->leave_type, [Leave::TYPE_ANNUAL, Leave::TYPE_HALF_AM, Leave::TYPE_HALF_PM])) { $year = \Carbon\Carbon::parse($leave->start_date)->year; $balance = $this->getOrCreateBalance($tenantId, $leave->user_id, $year); $balance->restoreLeave($leave->days); } } $leave->status = Leave::STATUS_CANCELLED; $leave->reject_reason = $reason; $leave->updated_by = $userId; $leave->save(); return $leave->fresh(['user:id,name,email']); }); } /** * 전체 직원 휴가 사용현황 목록 조회 * TenantUserProfile 기준으로 전체 직원 조회 후 LeaveBalance LEFT JOIN */ public function getAllBalances(array $params): LengthAwarePaginator { $tenantId = $this->tenantId(); $year = $params['year'] ?? now()->year; $query = TenantUserProfile::query() ->where('tenant_id', $tenantId) ->whereIn('employee_status', ['active', 'leave']) // 재직 + 휴직 직원 포함 ->with([ 'user:id,name,email', 'department:id,name', ]) ->addSelect([ 'tenant_user_profiles.*', 'leave_balance_total' => LeaveBalance::selectRaw('total_days') ->whereColumn('leave_balances.user_id', 'tenant_user_profiles.user_id') ->where('leave_balances.tenant_id', $tenantId) ->where('leave_balances.year', $year) ->limit(1), 'leave_balance_used' => LeaveBalance::selectRaw('used_days') ->whereColumn('leave_balances.user_id', 'tenant_user_profiles.user_id') ->where('leave_balances.tenant_id', $tenantId) ->where('leave_balances.year', $year) ->limit(1), ]); // 부서 필터 if (! empty($params['department_id'])) { $query->where('department_id', $params['department_id']); } // 검색 (사용자명) if (! empty($params['search'])) { $query->whereHas('user', function ($q) use ($params) { $q->where('name', 'like', '%'.$params['search'].'%'); }); } // 정렬 $sortBy = $params['sort_by'] ?? 'user_id'; $sortDir = $params['sort_dir'] ?? 'asc'; if ($sortBy === 'user_id') { $query->orderBy('user_id', $sortDir); } elseif ($sortBy === 'department') { $query->orderBy('department_id', $sortDir); } else { $query->orderBy($sortBy, $sortDir); } // 페이지네이션 $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 내 잔여 휴가 조회 */ public function getMyBalance(?int $year = null): LeaveBalance { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $year = $year ?? now()->year; return $this->getOrCreateBalance($tenantId, $userId, $year); } /** * 특정 사용자 잔여 휴가 조회 */ public function getUserBalance(int $userId, ?int $year = null): LeaveBalance { $tenantId = $this->tenantId(); $year = $year ?? now()->year; return $this->getOrCreateBalance($tenantId, $userId, $year); } /** * 잔여 휴가 설정 */ public function setBalance(int $userId, int $year, float $totalDays): LeaveBalance { $tenantId = $this->tenantId(); $balance = LeaveBalance::query() ->where('tenant_id', $tenantId) ->where('user_id', $userId) ->where('year', $year) ->first(); if ($balance) { $balance->total_days = $totalDays; $balance->save(); } else { $balance = LeaveBalance::create([ 'tenant_id' => $tenantId, 'user_id' => $userId, 'year' => $year, 'total_days' => $totalDays, 'used_days' => 0, ]); } return $balance->fresh(); } /** * 잔여 휴가 조회 또는 생성 */ private function getOrCreateBalance(int $tenantId, int $userId, int $year): LeaveBalance { $balance = LeaveBalance::query() ->where('tenant_id', $tenantId) ->where('user_id', $userId) ->where('year', $year) ->first(); if (! $balance) { $balance = LeaveBalance::create([ 'tenant_id' => $tenantId, 'user_id' => $userId, 'year' => $year, 'total_days' => 15, // 기본 연차 15일 'used_days' => 0, ]); } return $balance; } // ========================================================================= // 휴가 부여 관련 메서드 // ========================================================================= /** * 휴가 부여 이력 목록 조회 */ public function getGrants(array $params): LengthAwarePaginator { $tenantId = $this->tenantId(); $query = LeaveGrant::query() ->where('tenant_id', $tenantId) ->with([ 'user:id,name,email', 'user.tenantProfile' => function ($q) use ($tenantId) { $q->where('tenant_id', $tenantId) ->with('department:id,name'); }, 'creator:id,name', ]); // 사용자 필터 if (! empty($params['user_id'])) { $query->where('user_id', $params['user_id']); } // 부여 유형 필터 if (! empty($params['grant_type'])) { $query->where('grant_type', $params['grant_type']); } // 날짜 범위 필터 if (! empty($params['date_from'])) { $query->where('grant_date', '>=', $params['date_from']); } if (! empty($params['date_to'])) { $query->where('grant_date', '<=', $params['date_to']); } // 연도 필터 if (! empty($params['year'])) { $query->whereYear('grant_date', $params['year']); } // 부서 필터 if (! empty($params['department_id'])) { $query->whereHas('user.tenantProfile', function ($q) use ($params, $tenantId) { $q->where('tenant_id', $tenantId) ->where('department_id', $params['department_id']); }); } // 검색 (사용자명) if (! empty($params['search'])) { $query->whereHas('user', function ($q) use ($params) { $q->where('name', 'like', '%'.$params['search'].'%'); }); } // 정렬 $sortBy = $params['sort_by'] ?? 'grant_date'; $sortDir = $params['sort_dir'] ?? 'desc'; $query->orderBy($sortBy, $sortDir); // 페이지네이션 $perPage = $params['per_page'] ?? 20; return $query->paginate($perPage); } /** * 휴가 부여 */ public function storeGrant(array $data): LeaveGrant { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($data, $tenantId, $userId) { $grant = LeaveGrant::create([ 'tenant_id' => $tenantId, 'user_id' => $data['user_id'], 'grant_type' => $data['grant_type'], 'grant_date' => $data['grant_date'], 'grant_days' => $data['grant_days'], 'reason' => $data['reason'] ?? null, 'created_by' => $userId, ]); // 연차/월차인 경우 LeaveBalance의 total_days에 추가 if (in_array($data['grant_type'], [LeaveGrant::TYPE_ANNUAL, LeaveGrant::TYPE_MONTHLY])) { $year = \Carbon\Carbon::parse($data['grant_date'])->year; $balance = $this->getOrCreateBalance($tenantId, $data['user_id'], $year); $balance->total_days += $data['grant_days']; $balance->save(); } return $grant->fresh(['user:id,name,email', 'creator:id,name']); }); } /** * 휴가 부여 삭제 (soft delete) */ public function destroyGrant(int $id): bool { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); return DB::transaction(function () use ($id, $tenantId, $userId) { $grant = LeaveGrant::query() ->where('tenant_id', $tenantId) ->findOrFail($id); // 연차/월차인 경우 LeaveBalance의 total_days에서 차감 if (in_array($grant->grant_type, [LeaveGrant::TYPE_ANNUAL, LeaveGrant::TYPE_MONTHLY])) { $year = \Carbon\Carbon::parse($grant->grant_date)->year; $balance = LeaveBalance::query() ->where('tenant_id', $tenantId) ->where('user_id', $grant->user_id) ->where('year', $year) ->first(); if ($balance) { $balance->total_days = max(0, $balance->total_days - $grant->grant_days); $balance->save(); } } $grant->deleted_by = $userId; $grant->save(); $grant->delete(); return true; }); } }