permissions->allows($request->user(), Permission::UsersView), 403); $filters = $request->validate([ 'search' => ['nullable', 'string', 'max:160'], 'role' => ['nullable', Rule::enum(UserRole::class)], 'status' => ['nullable', Rule::enum(AccountStatus::class)], 'perPage' => ['nullable', 'integer', 'min:1', 'max:100'], 'page' => ['nullable', 'integer', 'min:1'], ]); $users = $this->directory->visibleTo($request->user(), $this->tenant->id()) ->with('directManager:id,name') ->when($filters['search'] ?? null, fn ($query, string $search) => $query->where(fn ($inner) => $inner->where('name', 'like', '%'.$search.'%')->orWhere('email', 'like', '%'.$search.'%')->orWhere('department', 'like', '%'.$search.'%'))) ->when($filters['role'] ?? null, fn ($query, string $role) => $query->where('role', $role)) ->when($filters['status'] ?? null, fn ($query, string $status) => $query->where('status', $status)) ->orderBy('name') ->paginate($filters['perPage'] ?? 25) ->through(fn (User $user) => $this->payload($user)); return response()->json(['data' => $users->items(), 'meta' => [ 'currentPage' => $users->currentPage(), 'lastPage' => $users->lastPage(), 'total' => $users->total(), ]]); } public function import(ImportUsersRequest $request): JsonResponse { abort_unless($this->permissions->allows($request->user(), Permission::UsersManage), 403); $result = $this->workforceImport->import($this->tenant->id(), $request->file('file')); $this->assignmentResolver->syncOrganization($this->tenant->id()); return response()->json(['data' => $result], 201); } public function template(Request $request): BinaryFileResponse { abort_unless($this->permissions->allows($request->user(), Permission::UsersManage), 403); return response()->download( $this->workforceTemplate->create(), 'workforce-import-template.xlsx', ['Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], )->deleteFileAfterSend(true); } public function update(UpdateUserRequest $request, string $user): JsonResponse { abort_unless($this->permissions->allows($request->user(), Permission::UsersManage), 403); $target = User::query()->where('organization_id', $this->tenant->id())->findOrFail($user); $data = $request->validated(); $nextRole = isset($data['role']) ? UserRole::from($data['role']) : $target->role; $nextStatus = isset($data['status']) ? AccountStatus::from($data['status']) : $target->status; if ($target->status === AccountStatus::Disabled && $nextStatus !== AccountStatus::Disabled) { $this->seatQuota->assertAvailable($this->tenant->id(), 'status'); } if ($target->role === UserRole::CourseDesigner && ($nextRole !== UserRole::CourseDesigner || $nextStatus !== AccountStatus::Active)) { $otherDesigners = User::query()->where('organization_id', $this->tenant->id())->whereKeyNot($target->getKey())->where('role', UserRole::CourseDesigner)->where('status', AccountStatus::Active)->exists(); if (! $otherDesigners) { throw ValidationException::withMessages(['role' => ['At least one active Course Designer is required.']]); } } $manager = null; if (array_key_exists('directManagerId', $data) && $data['directManagerId'] !== null) { if ((string) $data['directManagerId'] === (string) $target->getKey()) { throw ValidationException::withMessages(['directManagerId' => ['کاربر نمی‌تواند مدیر مستقیم خودش باشد.']]); } $manager = User::query() ->where('organization_id', $this->tenant->id()) ->where(fn ($query) => $query->whereIn('job_level', ['manager', 'senior_manager'])->orWhere('role', UserRole::Manager)) ->find($data['directManagerId']); if (! $manager) { throw ValidationException::withMessages(['directManagerId' => ['مدیر مستقیم انتخاب‌شده معتبر نیست.']]); } $ancestor = $manager; while ($ancestor?->direct_manager_id) { if ((string) $ancestor->direct_manager_id === (string) $target->getKey()) { throw ValidationException::withMessages(['directManagerId' => ['این انتخاب در ساختار سازمانی چرخه ایجاد می‌کند.']]); } $ancestor = User::query()->where('organization_id', $this->tenant->id())->find($ancestor->direct_manager_id); } } $firstName = array_key_exists('firstName', $data) ? trim($data['firstName']) : $target->first_name; $lastName = array_key_exists('lastName', $data) ? trim($data['lastName']) : $target->last_name; $changes = [ 'role' => $nextRole, 'status' => $nextStatus, ...(array_key_exists('firstName', $data) ? ['first_name' => $firstName] : []), ...(array_key_exists('lastName', $data) ? ['last_name' => $lastName] : []), ...(array_key_exists('email', $data) ? ['email' => mb_strtolower(trim($data['email']))] : []), ...(array_key_exists('department', $data) ? ['department' => $data['department'] ? trim($data['department']) : null] : []), ...(array_key_exists('jobLevel', $data) ? ['job_level' => $data['jobLevel']] : []), ...(array_key_exists('directManagerId', $data) ? ['direct_manager_id' => $manager?->getKey()] : []), ]; if (array_key_exists('firstName', $data) || array_key_exists('lastName', $data)) { $changes['name'] = trim(implode(' ', array_filter([$firstName, $lastName]))) ?: $target->name; } $target->update($changes); if ($nextStatus === AccountStatus::Disabled) { $target->tokens()->delete(); } $this->assignmentResolver->syncOrganization($this->tenant->id()); return response()->json(['data' => $this->payload($target->fresh('directManager:id,name'))]); } /** @return array */ private function payload(User $user): array { return [ 'id' => $user->getKey(), 'name' => $user->name, 'email' => $user->email, 'role' => $user->role->value, 'status' => $user->status->value, 'locale' => $user->locale, 'timezone' => $user->timezone, 'firstName' => $user->first_name, 'lastName' => $user->last_name, 'department' => $user->department, 'jobLevel' => $user->job_level, 'directManagerId' => $user->direct_manager_id, 'directManager' => $user->directManager ? ['id' => $user->directManager->getKey(), 'name' => $user->directManager->name] : null, 'createdAt' => $user->created_at?->toISOString(), ]; } }