diff --git a/app/Http/Controllers/Api/V1/LeaveController.php b/app/Http/Controllers/Api/V1/LeaveController.php index 9ef9eaf..0a9b40f 100644 --- a/app/Http/Controllers/Api/V1/LeaveController.php +++ b/app/Http/Controllers/Api/V1/LeaveController.php @@ -105,6 +105,17 @@ public function cancel(int $id, Request $request): JsonResponse }, __('message.leave.cancelled')); } + /** + * 전체 직원 휴가 사용현황 목록 + * GET /v1/leaves/balances + */ + public function balances(Request $request): JsonResponse + { + return ApiResponse::handle(function () use ($request) { + return $this->service->getAllBalances($request->all()); + }, __('message.fetched')); + } + /** * 내 잔여 휴가 조회 * GET /v1/leaves/balance diff --git a/app/Models/Members/User.php b/app/Models/Members/User.php index 8c6b2c7..4be158f 100644 --- a/app/Models/Members/User.php +++ b/app/Models/Members/User.php @@ -4,6 +4,7 @@ use App\Models\Commons\File; use App\Models\Tenants\Tenant; +use App\Models\Tenants\TenantUserProfile; use App\Traits\ModelTrait; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\SoftDeletes; @@ -77,4 +78,21 @@ public function tenantsMembership(): BelongsToMany ->withPivot(['is_active', 'is_default', 'joined_at', 'left_at', 'deleted_at']) ->wherePivotNull('deleted_at'); // 소프트삭제 제외 } + + /** + * 테넌트별 사용자 프로필 (전체) + */ + public function tenantProfiles() + { + return $this->hasMany(TenantUserProfile::class); + } + + /** + * 현재 테넌트의 사용자 프로필 + * 주의: 조회 시 tenant_id 조건 추가 필요 + */ + public function tenantProfile() + { + return $this->hasOne(TenantUserProfile::class); + } } diff --git a/app/Services/LeaveService.php b/app/Services/LeaveService.php index 070330c..458054b 100644 --- a/app/Services/LeaveService.php +++ b/app/Services/LeaveService.php @@ -4,6 +4,7 @@ use App\Models\Tenants\Leave; use App\Models\Tenants\LeaveBalance; +use App\Models\Tenants\TenantUserProfile; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\DB; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -307,6 +308,66 @@ public function cancel(int $id, ?string $reason = null): Leave }); } + /** + * 전체 직원 휴가 사용현황 목록 조회 + * 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) + ->where('employee_status', 'active') + ->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); + } + /** * 내 잔여 휴가 조회 */ diff --git a/routes/api.php b/routes/api.php index 401c882..62eef4a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -304,6 +304,7 @@ Route::prefix('leaves')->group(function () { Route::get('', [LeaveController::class, 'index'])->name('v1.leaves.index'); Route::post('', [LeaveController::class, 'store'])->name('v1.leaves.store'); + Route::get('/balances', [LeaveController::class, 'balances'])->name('v1.leaves.balances'); Route::get('/balance', [LeaveController::class, 'balance'])->name('v1.leaves.balance'); Route::get('/balance/{userId}', [LeaveController::class, 'userBalance'])->name('v1.leaves.userBalance'); Route::put('/balance', [LeaveController::class, 'setBalance'])->name('v1.leaves.setBalance');