Files
sam-api/app/Services/LeaveService.php

386 lines
12 KiB
PHP
Raw Normal View History

<?php
namespace App\Services;
use App\Models\Tenants\Leave;
use App\Models\Tenants\LeaveBalance;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class LeaveService extends Service
{
/**
* 휴가 목록 조회
*/
public function index(array $params): LengthAwarePaginator
{
$tenantId = $this->tenantId();
$query = Leave::query()
->where('tenant_id', $tenantId)
->with(['user:id,name,email', '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', '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']);
});
}
/**
* 잔여 휴가 조회
*/
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;
}
}