parent
b30b1fc4fa
commit
8e827de35f
8 changed files with 376 additions and 8 deletions
@ -0,0 +1,141 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace App\Components\User; |
||||||
|
|
||||||
|
use Livewire\Component; |
||||||
|
use Livewire\Attributes\On; |
||||||
|
use Livewire\WithPagination; |
||||||
|
use Illuminate\Validation\Rule; |
||||||
|
use App\Models\User; |
||||||
|
use Spatie\Permission\Models\Role; |
||||||
|
|
||||||
|
class Manager extends Component |
||||||
|
{ |
||||||
|
use WithPagination; |
||||||
|
|
||||||
|
public array $perPageOptions = [10, 25, 50, 100]; |
||||||
|
public int $perPage = 10; |
||||||
|
protected string $paginationTheme = 'bootstrap'; |
||||||
|
public string $mode = 'index'; |
||||||
|
public ?int $editingId = null; |
||||||
|
public string $search = ''; |
||||||
|
|
||||||
|
// Form fields |
||||||
|
public string $fullname = ''; |
||||||
|
public string $email = ''; |
||||||
|
public string $password = ''; |
||||||
|
public string $password_confirmation = ''; |
||||||
|
public array $roles = []; |
||||||
|
public array $allRoles = []; |
||||||
|
|
||||||
|
public function mount(): void |
||||||
|
{ |
||||||
|
$this->allRoles = Role::all()->toArray(); |
||||||
|
} |
||||||
|
|
||||||
|
public function updatedPerPage(int $value): void |
||||||
|
{ |
||||||
|
$this->resetPage(); |
||||||
|
} |
||||||
|
|
||||||
|
public function updatedSearch(string $value): void |
||||||
|
{ |
||||||
|
$this->resetPage(); |
||||||
|
} |
||||||
|
|
||||||
|
#[On('userSaved')] |
||||||
|
public function showIndex(): void |
||||||
|
{ |
||||||
|
$this->mode = 'index'; |
||||||
|
$this->resetPage(); |
||||||
|
$this->resetForm(); |
||||||
|
} |
||||||
|
|
||||||
|
public function showForm(?int $id = null): void |
||||||
|
{ |
||||||
|
$this->mode = 'form'; |
||||||
|
$this->editingId = $id; |
||||||
|
|
||||||
|
if ($id) { |
||||||
|
$user = User::with('roles')->findOrFail($id); |
||||||
|
$this->fullname = $user->fullname; |
||||||
|
$this->email = $user->email; |
||||||
|
$this->roles = $user->roles->pluck('name')->toArray(); |
||||||
|
} else { |
||||||
|
$this->resetForm(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
protected function resetForm(): void |
||||||
|
{ |
||||||
|
$this->fullname = ''; |
||||||
|
$this->email = ''; |
||||||
|
$this->password = ''; |
||||||
|
$this->password_confirmation = ''; |
||||||
|
$this->roles = []; |
||||||
|
$this->editingId = null; |
||||||
|
} |
||||||
|
|
||||||
|
public function delete(int $id): void |
||||||
|
{ |
||||||
|
User::findOrFail($id)->delete(); |
||||||
|
session()->flash('message', 'User deleted.'); |
||||||
|
$this->resetPage(); |
||||||
|
} |
||||||
|
|
||||||
|
public function save(): void |
||||||
|
{ |
||||||
|
$rules = [ |
||||||
|
'fullname' => ['required', 'string', 'max:255'], |
||||||
|
'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($this->editingId)], |
||||||
|
'roles' => ['nullable', 'array'], |
||||||
|
'roles.*' => ['string', Rule::exists('roles', 'name')], |
||||||
|
]; |
||||||
|
|
||||||
|
if (! $this->editingId) { |
||||||
|
$rules['password'] = ['required', 'string', 'min:6', 'confirmed']; |
||||||
|
} elseif ($this->password) { |
||||||
|
$rules['password'] = ['string', 'min:6', 'confirmed']; |
||||||
|
} |
||||||
|
|
||||||
|
$this->validate($rules); |
||||||
|
|
||||||
|
$user = $this->editingId |
||||||
|
? User::findOrFail($this->editingId) |
||||||
|
: new User; |
||||||
|
|
||||||
|
$user->fullname = $this->fullname; |
||||||
|
$user->email = $this->email; |
||||||
|
|
||||||
|
if ($this->password) { |
||||||
|
$user->password = $this->password; |
||||||
|
} |
||||||
|
|
||||||
|
$user->save(); |
||||||
|
$user->syncRoles($this->roles); |
||||||
|
|
||||||
|
session()->flash('message', 'User saved.'); |
||||||
|
$this->dispatch('userSaved'); |
||||||
|
} |
||||||
|
|
||||||
|
public function render() |
||||||
|
{ |
||||||
|
$users = User::with('roles') |
||||||
|
->when( |
||||||
|
$this->search, |
||||||
|
fn($q) => $q |
||||||
|
->where('fullname', 'like', "%{$this->search}%") |
||||||
|
->orWhere('email', 'like', "%{$this->search}%") |
||||||
|
) |
||||||
|
->orderByDesc('id') |
||||||
|
->paginate($this->perPage); |
||||||
|
|
||||||
|
return view('components.user.manager', [ |
||||||
|
'users' => $users, |
||||||
|
'allRoles' => collect($this->allRoles), |
||||||
|
'title' => $this->mode === 'index' |
||||||
|
? 'Users' |
||||||
|
: ($this->editingId ? 'Edit User' : 'Create User'), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
<?php |
||||||
|
namespace App\Http\Controllers; |
||||||
|
|
||||||
|
use App\Http\Controllers\Controller; |
||||||
|
use Spatie\Permission\Models\Role; |
||||||
|
|
||||||
|
class UserController extends Controller |
||||||
|
{ |
||||||
|
public function manager() |
||||||
|
{ |
||||||
|
return view('admin.users.manager'); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,5 @@ |
|||||||
|
@extends('layouts.app') |
||||||
|
|
||||||
|
@section('content') |
||||||
|
<livewire:user.manager /> |
||||||
|
@endsection |
@ -0,0 +1,184 @@ |
|||||||
|
{{-- resources/views/components/user/manager.blade.php --}} |
||||||
|
<div class="row"> |
||||||
|
<div class="col-12"> |
||||||
|
<div class="card mb-4"> |
||||||
|
{{-- Header --}} |
||||||
|
<div class="card-header d-flex justify-content-between align-items-center pb-0"> |
||||||
|
<h6 class="mb-0">{{ $title }}</h6> |
||||||
|
@if ($mode === 'index') |
||||||
|
<div class="d-flex align-items-center"> |
||||||
|
<div class="me-2"> |
||||||
|
<select wire:model="perPage" wire:change="resetPage" |
||||||
|
class="form-select form-select-sm" style="width:100px;display:inline-block"> |
||||||
|
@foreach ($perPageOptions as $opt) |
||||||
|
<option value="{{ $opt }}">{{ $opt }}</option> |
||||||
|
@endforeach |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
<div class="input-group input-group-outline me-2"> |
||||||
|
<input type="text" |
||||||
|
wire:model.debounce.500ms="search" |
||||||
|
wire:keydown.enter="resetPage" |
||||||
|
class="form-control form-control-sm" |
||||||
|
placeholder="Search name or email..."> |
||||||
|
</div> |
||||||
|
<button wire:click="resetPage" |
||||||
|
class="btn btn-sm btn-outline-success me-2 m-0"> |
||||||
|
<i class="fas fa-search"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
@else |
||||||
|
<button wire:click="showIndex" class="btn btn-sm btn-secondary"> |
||||||
|
← Back to list |
||||||
|
</button> |
||||||
|
@endif |
||||||
|
</div> |
||||||
|
|
||||||
|
{{-- Body --}} |
||||||
|
<div class="card-body p-3"> |
||||||
|
@if (session()->has('message')) |
||||||
|
<div class="alert alert-success">{{ session('message') }}</div> |
||||||
|
@endif |
||||||
|
|
||||||
|
@if ($mode === 'index') |
||||||
|
{{-- Skeleton with pagination placeholders --}} |
||||||
|
<x-skeleton-table |
||||||
|
:columns="6" |
||||||
|
:rows="10" |
||||||
|
height="3.5rem" |
||||||
|
:pages="5" |
||||||
|
/> |
||||||
|
|
||||||
|
{{-- Actual table --}} |
||||||
|
<div wire:loading.remove class="table-responsive"> |
||||||
|
<table class="table table-sm align-middle mb-0"> |
||||||
|
<thead> |
||||||
|
<tr style="height:3.5rem"> |
||||||
|
<th class="text-center" style="width:5%;">ID</th> |
||||||
|
<th style="width:25%;">Full Name</th> |
||||||
|
<th style="width:30%;">Email</th> |
||||||
|
<th style="width:20%;">Role</th> |
||||||
|
<th style="width:20%;">Phone</th> |
||||||
|
<th class="text-center" style="width:5%;">Actions</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
@forelse($users as $u) |
||||||
|
<tr style="height:3.5rem"> |
||||||
|
<td class="text-center">{{ $u->id }}</td> |
||||||
|
<td>{{ $u->fullname }}</td> |
||||||
|
<td>{{ $u->email }}</td> |
||||||
|
<td>{{ $u->roles->pluck('name')->join(', ') }}</td> |
||||||
|
<td>{{ $u->phone }}</td> |
||||||
|
<td class="text-center"> |
||||||
|
<button wire:click="showForm({{ $u->id }})" |
||||||
|
class="btn btn-sm btn-outline-info me-1"> |
||||||
|
Edit |
||||||
|
</button> |
||||||
|
<button x-data |
||||||
|
@click.prevent="if(confirm('Delete user?')){ $wire.delete({{ $u->id }}) }" |
||||||
|
class="btn btn-sm btn-danger"> |
||||||
|
Delete |
||||||
|
</button> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
@empty |
||||||
|
<tr> |
||||||
|
<td colspan="6" class="text-center">No users found.</td> |
||||||
|
</tr> |
||||||
|
@endforelse |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
|
||||||
|
{{-- Real pagination --}} |
||||||
|
<div wire:loading.remove class="mt-3"> |
||||||
|
@if ($users->lastPage() > 1) |
||||||
|
{{ $users->links() }} |
||||||
|
@endif |
||||||
|
</div> |
||||||
|
@else |
||||||
|
{{-- Form create/edit --}} |
||||||
|
<x-skeleton-form :fields="4" :button-count="2" /> |
||||||
|
<div wire:loading.remove> |
||||||
|
<form> |
||||||
|
<div class="mb-3"> |
||||||
|
<label class="form-label">Name</label> |
||||||
|
<input type="text" |
||||||
|
wire:model.defer="fullname" |
||||||
|
class="form-control form-control-sm" |
||||||
|
placeholder="fullname"> |
||||||
|
@error('fullname') |
||||||
|
<div class="text-danger small">{{ $message }}</div> |
||||||
|
@enderror |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
<label class="form-label">Email</label> |
||||||
|
<input type="email" |
||||||
|
wire:model.defer="email" |
||||||
|
class="form-control form-control-sm" |
||||||
|
placeholder="Email"> |
||||||
|
@error('email') |
||||||
|
<div class="text-danger small">{{ $message }}</div> |
||||||
|
@enderror |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
<label class="form-label"> |
||||||
|
Password |
||||||
|
@if ($editingId) |
||||||
|
<small>(leave blank to keep current)</small> |
||||||
|
@endif |
||||||
|
</label> |
||||||
|
<input type="password" |
||||||
|
wire:model.defer="password" |
||||||
|
class="form-control form-control-sm" |
||||||
|
placeholder="Password"> |
||||||
|
@error('password') |
||||||
|
<div class="text-danger small">{{ $message }}</div> |
||||||
|
@enderror |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
<label class="form-label">Confirm Password</label> |
||||||
|
<input type="password" |
||||||
|
wire:model.defer="password_confirmation" |
||||||
|
class="form-control form-control-sm" |
||||||
|
placeholder="Confirm"> |
||||||
|
</div> |
||||||
|
<div class="mb-3"> |
||||||
|
<label class="form-label">Roles</label> |
||||||
|
<select wire:model.defer="roles" |
||||||
|
multiple |
||||||
|
class="form-select form-select-sm"> |
||||||
|
@foreach ($allRoles as $role) |
||||||
|
<option value="{{ $role['name'] }}">{{ ucfirst($role['name']) }}</option> |
||||||
|
@endforeach |
||||||
|
</select> |
||||||
|
@error('roles') |
||||||
|
<div class="text-danger small">{{ $message }}</div> |
||||||
|
@enderror |
||||||
|
</div> |
||||||
|
<div class="d-flex justify-content-end"> |
||||||
|
<button type="button" |
||||||
|
wire:click="showIndex" |
||||||
|
class="btn btn-secondary btn-sm me-2"> |
||||||
|
Cancel |
||||||
|
</button> |
||||||
|
<button type="button" |
||||||
|
@click.prevent="if(confirm('Do you really want to {{ $editingId ? 'update' : 'create' }} this user?')){ $wire.save() }" |
||||||
|
class="btn btn-primary btn-sm"> |
||||||
|
{{ $editingId ? 'Update' : 'Create' }} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
@endif |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
@script |
||||||
|
<script> |
||||||
|
document.title = @json($title); |
||||||
|
</script> |
||||||
|
@endscript |
||||||
|
</div> |
Loading…
Reference in new issue