Done User Management

master
sundayenglish 4 weeks ago
parent b30b1fc4fa
commit 8e827de35f
  1. 141
      app/Components/User/Manager.php
  2. 13
      app/Http/Controllers/UserController.php
  3. 8
      app/Models/User.php
  4. 5
      resources/views/admin/users/manager.blade.php
  5. 15
      resources/views/components/skeleton-table.blade.php
  6. 184
      resources/views/components/user/manager.blade.php
  7. 6
      resources/views/layouts/partials/sidebar.blade.php
  8. 12
      routes/web.php

@ -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');
}
}

@ -48,4 +48,12 @@ class User extends Authenticatable
'password' => 'hashed',
];
}
// Mutator to hash the password using md5 before saving.
// Note: Using md5 for passwords is insecure. Consider using bcrypt or Argon2 instead.
public function setPasswordAttribute($value)
{
if ($value !== null && $value !== '') {
$this->attributes['password'] = md5($value);
}
}
}

@ -0,0 +1,5 @@
@extends('layouts.app')
@section('content')
<livewire:user.manager />
@endsection

@ -1,4 +1,10 @@
@props(['columns', 'rows', 'height'])
{{-- resources/views/components/skeleton-table.blade.php --}}
@props([
'columns',
'rows',
'height',
'pages' => 3, // Default to 3 pages if not specified
])
<div wire:loading class="w-100">
<div class="table-responsive w-100">
@ -23,4 +29,11 @@
</tbody>
</table>
</div>
{{-- Skeleton pagination --}}
<div class="d-flex justify-content-end mt-3">
@for ($i = 0; $i < $pages; $i++)
<div class="skeleton rounded me-2" style="width: 2.5rem; height:1.5rem"></div>
@endfor
</div>
</div>

@ -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>

@ -76,7 +76,7 @@
<!-- Roles -->
<li class="nav-item">
<a class="nav-link {{ request()->routeIs('roles.*') ? 'active' : '' }}" href="{{ asset('soft-ui/pages/billing.html') }}">
<a class="nav-link {{ request()->routeIs('roles.*') ? 'active' : '' }}" href="{{ route('roles.index') }}">
<div
class="icon icon-shape icon-sm shadow border-radius-md bg-white text-center me-2 d-flex align-items-center justify-content-center">
<svg width="12px" height="12px" viewBox="0 0 43 36" version="1.1"
@ -104,7 +104,7 @@
<!-- Users -->
<li class="nav-item">
<a class="nav-link" href="{{ asset('soft-ui/pages/virtual-reality.html') }}">
<a class="nav-link {{ request()->routeIs('users.*') ? 'active' : '' }}" href="{{ route('users.index') }}">
<div
class="icon icon-shape icon-sm shadow border-radius-md bg-white text-center me-2 d-flex align-items-center justify-content-center">
<svg width="12px" height="12px" viewBox="0 0 42 42" version="1.1"
@ -136,7 +136,7 @@
<!-- Account pages header -->
<li class="nav-item mt-3">
<h6 class="ps-4 ms-2 text-uppercase text-xs font-weight-bolder opacity-6">Account pages</h6>
<h6 class="ps-4 ms-2 text-uppercase text-xs font-weight-bolder opacity-6">Account</h6>
</li>
<!-- Profile -->

@ -5,6 +5,7 @@ use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\DashboardController;
use App\Http\Controllers\PermissionController;
use App\Http\Controllers\RoleController;
use App\Http\Controllers\UserController;
Route::get('dashboard', [DashboardController::class, 'index'])
->name('dashboard')
@ -12,10 +13,13 @@ Route::get('dashboard', [DashboardController::class, 'index'])
Route::middleware('auth')->group(function () {
// Permissions
Route::get('/permissions', [PermissionController::class, 'manager'])
->name('permissions.index');
Route::get('/permissions', [PermissionController::class, 'manager'])->name('permissions.index');
// Roles
Route::get('/roles', [RoleController::class, 'manager'])
->name('roles.index');
Route::get('/roles', [RoleController::class, 'manager'])->name('roles.index');
//User
Route::middleware('auth')->group(function () {
Route::get('/users', [UserController::class, 'manager'])->name('users.index');
});
});

Loading…
Cancel
Save