162 خطوط
8.5 KiB
PHP
162 خطوط
8.5 KiB
PHP
<?php
|
|
|
|
namespace App\Modules\Identity\Http;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\User;
|
|
use App\Modules\Assignments\Application\AssignmentResolver;
|
|
use App\Modules\Identity\Application\RolePermissions;
|
|
use App\Modules\Identity\Application\UserDirectory;
|
|
use App\Modules\Identity\Application\WorkforceImport;
|
|
use App\Modules\Identity\Application\WorkforceTemplate;
|
|
use App\Modules\Identity\Domain\Enums\AccountStatus;
|
|
use App\Modules\Identity\Domain\Enums\Permission;
|
|
use App\Modules\Identity\Domain\Enums\UserRole;
|
|
use App\Modules\Identity\Http\Requests\ImportUsersRequest;
|
|
use App\Modules\Identity\Http\Requests\UpdateUserRequest;
|
|
use App\Modules\Subscriptions\Application\SeatQuota;
|
|
use App\Modules\Tenancy\Application\TenantContext;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
|
|
final class UserController extends Controller
|
|
{
|
|
public function __construct(
|
|
private readonly UserDirectory $directory,
|
|
private readonly TenantContext $tenant,
|
|
private readonly RolePermissions $permissions,
|
|
private readonly SeatQuota $seatQuota,
|
|
private readonly WorkforceImport $workforceImport,
|
|
private readonly WorkforceTemplate $workforceTemplate,
|
|
private readonly AssignmentResolver $assignmentResolver,
|
|
) {}
|
|
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
abort_unless($this->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<string, mixed> */
|
|
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(),
|
|
];
|
|
}
|
|
}
|