refactor(livewire): organizar componentes y vistas por dominio en subnamespaces
- app/Livewire: 34 componentes agrupados en Issues/, Projects/, Phases/, Companies/, Users/, Admin/, Inspections/, Layers/, Media/, Common/ (Client/, Reports/, Forms/, Actions/ ya estaban). Namespaces actualizados. - resources/views/livewire: vistas sueltas movidas a subcarpetas espejo (companies/, users/, phases/, roles/, inspections/, media/, common/); render() actualizado. - Referencias actualizadas sin romper nada: rutas (FQN, nombres de ruta intactos), tags <livewire:...>/@livewire() a alias con punto, y use de los tests. - No tocado: Volt de Breeze (auth/profile/navigation), y el portal cliente (user-nav/client-projects) que ya tenía referencias inconsistentes. Verificado: 69 rutas OK, vistas compilan, suite 69 passing (solo 2 pre-existentes sqlite). autoload regenerado con --ignore-platform-reqs (PHP 8.2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Users;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\User;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AdminUsers extends Component
|
||||
{
|
||||
public string $search = '';
|
||||
public $roles;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
abort_unless(Auth::user()->can('view users'), 403);
|
||||
$this->roles = Role::orderBy('name')->get();
|
||||
}
|
||||
|
||||
public function getUsersProperty()
|
||||
{
|
||||
return User::with('roles')
|
||||
->when($this->search, fn($q) =>
|
||||
$q->where(fn($q2) => $q2
|
||||
->where('name', 'like', '%' . $this->search . '%')
|
||||
->orWhere('email', 'like', '%' . $this->search . '%')))
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}
|
||||
|
||||
public function deleteUser(int $userId): void
|
||||
{
|
||||
if ($userId === Auth::id()) {
|
||||
$this->dispatch('notify', 'No puedes eliminarte a ti mismo.');
|
||||
return;
|
||||
}
|
||||
User::findOrFail($userId)->delete();
|
||||
$this->dispatch('notify', 'Usuario eliminado.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.users.admin-users');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Users;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Layout;
|
||||
use App\Models\User;
|
||||
use App\Models\Company;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
#[Layout('layouts.app')]
|
||||
class UserForm extends Component
|
||||
{
|
||||
public ?User $user = null;
|
||||
|
||||
// Información personal
|
||||
public string $title = '';
|
||||
public string $lastName = '';
|
||||
public string $firstName = '';
|
||||
|
||||
// Validación
|
||||
public string $userStatus = 'active';
|
||||
public string $validFrom = '';
|
||||
public string $validUntil = '';
|
||||
public string $formPassword = '';
|
||||
|
||||
// Contacto
|
||||
public ?int $companyId = null;
|
||||
public string $address = '';
|
||||
public string $phone = '';
|
||||
public string $email = '';
|
||||
|
||||
// Permisos
|
||||
public string $formRole = '';
|
||||
|
||||
// Preferencias
|
||||
public string $locale = 'es';
|
||||
|
||||
// Notas
|
||||
public string $notes = '';
|
||||
|
||||
// Catálogos
|
||||
public $roles;
|
||||
public $companies;
|
||||
|
||||
/** Idiomas disponibles (código => nombre + archivo de bandera). */
|
||||
public array $languages = [
|
||||
'es' => ['name' => 'Español', 'flag' => 'es.svg'],
|
||||
'en' => ['name' => 'English', 'flag' => 'gb.svg'],
|
||||
];
|
||||
|
||||
public function mount(?User $user = null): void
|
||||
{
|
||||
abort_unless(Auth::user()->can('create users') || Auth::user()->can('edit users'), 403);
|
||||
|
||||
$this->roles = Role::orderBy('name')->get();
|
||||
$this->companies = Company::where('estado', 'activo')->orderBy('name')->get();
|
||||
$this->formRole = $this->roles->first()?->name ?? '';
|
||||
|
||||
if ($user && $user->exists) {
|
||||
$this->user = $user;
|
||||
$this->title = $user->title ?? '';
|
||||
$this->lastName = $user->last_name ?? '';
|
||||
$this->firstName = $user->first_name ?? '';
|
||||
$this->userStatus = $user->status ?? 'active';
|
||||
$this->validFrom = $user->valid_from?->format('Y-m-d') ?? '';
|
||||
$this->validUntil = $user->valid_until?->format('Y-m-d') ?? '';
|
||||
$this->companyId = $user->company_id;
|
||||
$this->address = $user->address ?? '';
|
||||
$this->phone = $user->phone ?? '';
|
||||
$this->email = $user->email;
|
||||
$this->notes = $user->notes ?? '';
|
||||
$this->formRole = $user->roles->first()?->name ?? $this->formRole;
|
||||
$this->locale = $user->locale ?? $this->locale;
|
||||
}
|
||||
}
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
$id = $this->user?->id ?? 'NULL';
|
||||
$rules = [
|
||||
'lastName' => 'required|string|max:100',
|
||||
'firstName' => 'required|string|max:100',
|
||||
'title' => 'nullable|string|max:20',
|
||||
'userStatus' => 'required|in:active,inactive,suspended',
|
||||
'validFrom' => 'nullable|date',
|
||||
'validUntil' => 'nullable|date|after_or_equal:validFrom',
|
||||
'companyId' => 'required|exists:companies,id',
|
||||
'address' => 'nullable|string',
|
||||
'phone' => 'nullable|string|max:30',
|
||||
'email' => "required|email|max:255|unique:users,email,{$id}",
|
||||
'formRole' => 'required|exists:roles,name',
|
||||
'locale' => 'required|in:' . implode(',', array_keys($this->languages)),
|
||||
];
|
||||
|
||||
if (!$this->user) {
|
||||
$rules['formPassword'] = ['required', Password::min(8)->letters()->mixedCase()->numbers()];
|
||||
} elseif ($this->formPassword !== '') {
|
||||
$rules['formPassword'] = [Password::min(8)->letters()->mixedCase()->numbers()];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
protected $validationAttributes = [
|
||||
'lastName' => 'apellidos',
|
||||
'firstName' => 'nombre',
|
||||
'userStatus' => 'estado',
|
||||
'validFrom' => 'fecha de inicio',
|
||||
'validUntil' => 'fecha de fin',
|
||||
'companyId' => 'empresa',
|
||||
'formPassword'=> 'contraseña',
|
||||
'formRole' => 'rol',
|
||||
'locale' => 'idioma',
|
||||
];
|
||||
|
||||
public function copyCompanyAddress(): void
|
||||
{
|
||||
if (!$this->companyId) return;
|
||||
$company = Company::find($this->companyId);
|
||||
if ($company?->address) {
|
||||
$this->address = $company->address;
|
||||
}
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
if ($this->user && $this->user->id === Auth::id()
|
||||
&& $this->user->hasRole('Admin') && $this->formRole !== 'Admin') {
|
||||
$this->addError('formRole', 'No puedes quitarte el rol Admin a ti mismo.');
|
||||
return;
|
||||
}
|
||||
|
||||
$fullName = trim($this->firstName . ' ' . $this->lastName);
|
||||
|
||||
$data = [
|
||||
'name' => $fullName,
|
||||
'title' => $this->title ?: null,
|
||||
'first_name' => $this->firstName,
|
||||
'last_name' => $this->lastName,
|
||||
'status' => $this->userStatus,
|
||||
'valid_from' => $this->validFrom ?: null,
|
||||
'valid_until'=> $this->validUntil ?: null,
|
||||
'company_id' => $this->companyId,
|
||||
'address' => $this->address ?: null,
|
||||
'phone' => $this->phone ?: null,
|
||||
'email' => $this->email,
|
||||
'notes' => $this->notes ?: null,
|
||||
'locale' => $this->locale,
|
||||
];
|
||||
|
||||
if ($this->formPassword !== '') {
|
||||
$data['password'] = Hash::make($this->formPassword);
|
||||
}
|
||||
|
||||
if ($this->user && $this->user->exists) {
|
||||
$this->user->update($data);
|
||||
$this->user->syncRoles([$this->formRole]);
|
||||
session()->flash('notify', 'Usuario actualizado correctamente.');
|
||||
} else {
|
||||
$user = User::create($data);
|
||||
$user->assignRole($this->formRole);
|
||||
session()->flash('notify', 'Usuario creado correctamente.');
|
||||
}
|
||||
|
||||
$this->redirect(route('admin.users'), navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.users.user-form');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Users;
|
||||
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Filters\SelectFilter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use App\Models\User;
|
||||
|
||||
class UserTable extends DataTableComponent
|
||||
{
|
||||
protected $model = User::class;
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setPrimaryKey('id')
|
||||
->setDefaultSort('name', 'asc')
|
||||
->setSortingPillsEnabled(false)
|
||||
->setAdditionalSelects([
|
||||
'users.id as id',
|
||||
'users.email as email',
|
||||
'users.email_verified_at as email_verified_at',
|
||||
'users.status as status',
|
||||
'users.phone as phone',
|
||||
'users.company_id as company_id',
|
||||
'users.created_at as created_at',
|
||||
]);
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return User::with(['roles', 'company']);
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Usuario', 'name')
|
||||
->sortable()
|
||||
->searchable()
|
||||
->format(function ($value, $row) {
|
||||
$initial = strtoupper(mb_substr($value, 0, 1));
|
||||
$html = '<div class="flex items-center gap-3">';
|
||||
$html .= '<div class="avatar placeholder shrink-0">
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-8">
|
||||
<span class="text-xs font-semibold">'.$initial.'</span>
|
||||
</div>
|
||||
</div>';
|
||||
$html .= '<div>';
|
||||
$html .= '<p class="font-semibold text-sm leading-tight">'.e($value).'</p>';
|
||||
$html .= '<p class="text-xs text-gray-500">'.e($row->email).'</p>';
|
||||
$html .= '</div></div>';
|
||||
return $html;
|
||||
})
|
||||
->html(),
|
||||
|
||||
Column::make('Empresa')
|
||||
->label(fn ($row) =>
|
||||
$row->company
|
||||
? '<span class="text-sm">'.e($row->company->name).'</span>'
|
||||
: '<span class="text-gray-300 text-sm">—</span>'
|
||||
)
|
||||
->html(),
|
||||
|
||||
Column::make('Rol')
|
||||
->label(function ($row) {
|
||||
if ($row->roles->isEmpty()) {
|
||||
return '<span class="badge badge-sm badge-ghost">Sin rol</span>';
|
||||
}
|
||||
return $row->roles->map(fn ($role) =>
|
||||
'<span class="badge badge-sm '.($role->name === 'Admin' ? 'badge-error' : 'badge-primary').'">'.e($role->name).'</span>'
|
||||
)->implode(' ');
|
||||
})
|
||||
->html(),
|
||||
|
||||
Column::make('Estado', 'status')
|
||||
->sortable()
|
||||
->format(function ($value) {
|
||||
$map = [
|
||||
'active' => ['badge-success', 'Activo'],
|
||||
'inactive' => ['badge-ghost', 'Inactivo'],
|
||||
'suspended' => ['badge-error', 'Suspendido'],
|
||||
];
|
||||
[$cls, $label] = $map[$value ?? 'active'] ?? ['badge-ghost', ucfirst($value ?? '')];
|
||||
return '<span class="badge badge-sm '.$cls.'">'.$label.'</span>';
|
||||
})
|
||||
->html(),
|
||||
|
||||
Column::make('Verificado', 'email_verified_at')
|
||||
->sortable()
|
||||
->format(fn ($value) =>
|
||||
$value
|
||||
? '<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
|
||||
: '<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-warning" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
|
||||
)
|
||||
->html(),
|
||||
|
||||
Column::make('Acciones')
|
||||
->label(function ($row) {
|
||||
$ver = route('admin.users.show', $row->id);
|
||||
$editar = route('admin.users.edit', $row->id);
|
||||
$name = addslashes($row->name);
|
||||
$isSelf = $row->id === Auth::id();
|
||||
|
||||
$html = '<div class="flex items-center justify-end gap-1">';
|
||||
$html .= '<a href="'.$ver.'" class="btn btn-xs btn-outline" title="Ver" wire:navigate>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
|
||||
</a>';
|
||||
$html .= '<a href="'.$editar.'" class="btn btn-xs btn-outline btn-info" title="Editar" wire:navigate>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||
</a>';
|
||||
if (! $isSelf) {
|
||||
$html .= '<button wire:click="deleteUser('.$row->id.')"
|
||||
wire:confirm="¿Eliminar a \''.$name.'\'? Se perderán todos sus datos."
|
||||
class="btn btn-xs btn-outline btn-error" title="Eliminar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
</button>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
})
|
||||
->html(),
|
||||
];
|
||||
}
|
||||
|
||||
public function filters(): array
|
||||
{
|
||||
$roleOptions = [''] + Role::orderBy('name')->pluck('name', 'name')->prepend('Rol: todos', '')->toArray();
|
||||
|
||||
return [
|
||||
SelectFilter::make('Rol')
|
||||
->options($roleOptions)
|
||||
->filter(fn (Builder $query, string $value) =>
|
||||
$query->whereHas('roles', fn ($q) => $q->where('name', $value))
|
||||
),
|
||||
|
||||
SelectFilter::make('Estado', 'status')
|
||||
->options([
|
||||
'' => 'Estado: todos',
|
||||
'active' => 'Activo',
|
||||
'inactive' => 'Inactivo',
|
||||
'suspended' => 'Suspendido',
|
||||
])
|
||||
->filter(fn (Builder $query, string $value) => $query->where('status', $value)),
|
||||
];
|
||||
}
|
||||
|
||||
public function deleteUser(int $id): void
|
||||
{
|
||||
if ($id === Auth::id()) return;
|
||||
User::findOrFail($id)->delete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Users;
|
||||
|
||||
use Livewire\Component;
|
||||
use Livewire\Attributes\Layout;
|
||||
use App\Models\User;
|
||||
use App\Models\Project;
|
||||
use App\Models\Inspection;
|
||||
use App\Models\Issue;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
|
||||
#[Layout('layouts.app')]
|
||||
class UserView extends Component
|
||||
{
|
||||
public User $user;
|
||||
public string $activeTab = 'ficha';
|
||||
|
||||
// Projects tab
|
||||
public ?int $addProjectId = null;
|
||||
public string $addProjectRole = '';
|
||||
public $availableProjects;
|
||||
|
||||
// Notes tab
|
||||
public string $notes = '';
|
||||
public bool $editingNotes = false;
|
||||
|
||||
// Recent activity (loaded once)
|
||||
public $recentInspections;
|
||||
public $recentIssues;
|
||||
|
||||
public function mount(User $user): void
|
||||
{
|
||||
abort_unless(Auth::user()->can('view users'), 403);
|
||||
|
||||
$this->user = $user->load(['roles', 'company', 'projects.phases']);
|
||||
$this->notes = $user->notes ?? '';
|
||||
|
||||
$this->loadAvailableProjects();
|
||||
$this->loadActivity();
|
||||
}
|
||||
|
||||
private function loadAvailableProjects(): void
|
||||
{
|
||||
$assignedIds = $this->user->projects->pluck('id');
|
||||
$this->availableProjects = Project::whereNotIn('id', $assignedIds)
|
||||
->orderBy('name')->get();
|
||||
}
|
||||
|
||||
private function loadActivity(): void
|
||||
{
|
||||
$this->recentInspections = Inspection::where('user_id', $this->user->id)
|
||||
->with(['feature.layer.phase.project', 'template'])
|
||||
->latest()->take(8)->get();
|
||||
|
||||
$this->recentIssues = Issue::where('reported_by', $this->user->id)
|
||||
->with(['feature', 'project'])
|
||||
->latest()->take(8)->get();
|
||||
}
|
||||
|
||||
// ── Tabs ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public function setTab(string $tab): void
|
||||
{
|
||||
$this->activeTab = $tab;
|
||||
}
|
||||
|
||||
// ── Projects ──────────────────────────────────────────────────────────────
|
||||
|
||||
public function assignProject(): void
|
||||
{
|
||||
$this->validate([
|
||||
'addProjectId' => 'required|exists:projects,id',
|
||||
'addProjectRole' => 'nullable|string|max:100',
|
||||
], [], ['addProjectId' => 'proyecto', 'addProjectRole' => 'rol en proyecto']);
|
||||
|
||||
$this->user->projects()->attach($this->addProjectId, [
|
||||
'role_in_project' => $this->addProjectRole ?: null,
|
||||
]);
|
||||
|
||||
$this->user->load('projects.phases');
|
||||
$this->addProjectId = null;
|
||||
$this->addProjectRole = '';
|
||||
$this->loadAvailableProjects();
|
||||
$this->dispatch('notify', 'Proyecto asignado.');
|
||||
}
|
||||
|
||||
public function removeProject(int $projectId): void
|
||||
{
|
||||
$this->user->projects()->detach($projectId);
|
||||
$this->user->load('projects.phases');
|
||||
$this->loadAvailableProjects();
|
||||
$this->dispatch('notify', 'Proyecto desasignado.');
|
||||
}
|
||||
|
||||
// ── Permissions (direct, per user) ─────────────────────────────────────────
|
||||
|
||||
public function togglePermission(string $name): void
|
||||
{
|
||||
if ($this->user->hasDirectPermission($name)) {
|
||||
$this->user->revokePermissionTo($name);
|
||||
} else {
|
||||
$this->user->givePermissionTo($name);
|
||||
}
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
$this->user->load('roles', 'permissions');
|
||||
$this->dispatch('notify', 'Permisos del usuario actualizados');
|
||||
}
|
||||
|
||||
public function setUserGroup(string $group, bool $enabled): void
|
||||
{
|
||||
foreach (Permission::where('group', $group)->pluck('name') as $name) {
|
||||
if ($enabled) {
|
||||
if (! $this->user->hasPermissionTo($name)) {
|
||||
$this->user->givePermissionTo($name);
|
||||
}
|
||||
} elseif ($this->user->hasDirectPermission($name)) {
|
||||
$this->user->revokePermissionTo($name);
|
||||
}
|
||||
}
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
$this->user->load('roles', 'permissions');
|
||||
$this->dispatch('notify', $enabled ? 'Permisos del grupo concedidos' : 'Permisos directos del grupo quitados');
|
||||
}
|
||||
|
||||
// ── Notes ─────────────────────────────────────────────────────────────────
|
||||
|
||||
public function saveNotes(): void
|
||||
{
|
||||
$this->validate(['notes' => 'nullable|string']);
|
||||
$this->user->update(['notes' => $this->notes ?: null]);
|
||||
$this->editingNotes = false;
|
||||
$this->dispatch('notify', 'Notas guardadas.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$order = [
|
||||
'Proyectos', 'Fases y progreso', 'Capas y elementos', 'Inspecciones',
|
||||
'Incidencias', 'Empresas', 'Usuarios', 'Roles', 'Informes', 'Archivos', 'General',
|
||||
];
|
||||
|
||||
$grouped = Permission::orderBy('name')->get()
|
||||
->groupBy(fn ($perm) => $perm->group ?: 'General')
|
||||
->sortBy(function ($perms, $section) use ($order) {
|
||||
$i = array_search($section, $order, true);
|
||||
return $i === false ? 999 : $i;
|
||||
});
|
||||
|
||||
return view('livewire.users.user-view', [
|
||||
'grouped' => $grouped,
|
||||
'directPerms' => $this->user->getDirectPermissions()->pluck('name')->toArray(),
|
||||
'rolePerms' => $this->user->getPermissionsViaRoles()->pluck('name')->toArray(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user