New_Micro_Learning/backend/app/Modules/Identity/Http/UserController.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(),
];
}
}