parent
eb88911dfc
commit
8ca9f346d9
5 changed files with 343 additions and 4 deletions
@ -0,0 +1,137 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace App\Components\Role; |
||||||
|
|
||||||
|
use Livewire\Component; |
||||||
|
use Livewire\Attributes\On; |
||||||
|
use Livewire\WithPagination; |
||||||
|
use Spatie\Permission\Models\Role; |
||||||
|
|
||||||
|
class Manager extends Component |
||||||
|
{ |
||||||
|
use WithPagination; |
||||||
|
|
||||||
|
/** |
||||||
|
* Các tùy chọn số bản ghi mỗi trang. |
||||||
|
* |
||||||
|
* @var int[] |
||||||
|
*/ |
||||||
|
public array $perPageOptions = [5, 10, 25, 50, 100]; |
||||||
|
|
||||||
|
/** |
||||||
|
* Số bản ghi mỗi trang (mặc định). |
||||||
|
*/ |
||||||
|
public int $perPage = 10; |
||||||
|
|
||||||
|
/** |
||||||
|
* Giao diện phân trang (bootstrap hoặc tailwind). |
||||||
|
*/ |
||||||
|
protected string $paginationTheme = 'bootstrap'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Chế độ UI: 'index' (danh sách) hoặc 'form' (tạo/sửa). |
||||||
|
*/ |
||||||
|
public string $mode = 'index'; |
||||||
|
|
||||||
|
/** |
||||||
|
* ID của role đang sửa (null khi tạo mới). |
||||||
|
*/ |
||||||
|
public ?int $editingId = null; |
||||||
|
|
||||||
|
/** |
||||||
|
* Chuỗi tìm kiếm hiện tại. |
||||||
|
*/ |
||||||
|
public string $search = ''; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tên role dùng trong form. |
||||||
|
*/ |
||||||
|
public string $name = ''; |
||||||
|
|
||||||
|
/** |
||||||
|
* Khi thay đổi số bản ghi mỗi trang, reset về trang 1. |
||||||
|
*/ |
||||||
|
public function updatedPerPage(int $value): void |
||||||
|
{ |
||||||
|
$this->resetPage(); |
||||||
|
} |
||||||
|
|
||||||
|
#[On('roleSaved')] |
||||||
|
public function showIndex(): void |
||||||
|
{ |
||||||
|
$this->mode = 'index'; |
||||||
|
$this->resetPage(); |
||||||
|
$this->name = ''; |
||||||
|
$this->editingId = null; |
||||||
|
} |
||||||
|
|
||||||
|
public function updatedSearch(string $value): void |
||||||
|
{ |
||||||
|
$this->resetPage(); |
||||||
|
} |
||||||
|
|
||||||
|
public function showForm(?int $id = null): void |
||||||
|
{ |
||||||
|
$this->mode = 'form'; |
||||||
|
$this->editingId = $id; |
||||||
|
$this->name = $id |
||||||
|
? Role::findOrFail($id)->name |
||||||
|
: ''; |
||||||
|
} |
||||||
|
|
||||||
|
public function delete(int $id): void |
||||||
|
{ |
||||||
|
Role::findOrFail($id)->delete(); |
||||||
|
session()->flash('message', 'Role deleted.'); |
||||||
|
$this->resetPage(); |
||||||
|
} |
||||||
|
|
||||||
|
public function save(): void |
||||||
|
{ |
||||||
|
$uniqueRule = 'unique:roles,name' |
||||||
|
. ($this->editingId ? ',' . $this->editingId : ''); |
||||||
|
|
||||||
|
$this->validate([ |
||||||
|
'name' => "required|string|{$uniqueRule}", |
||||||
|
]); |
||||||
|
|
||||||
|
Role::updateOrCreate( |
||||||
|
['id' => $this->editingId], |
||||||
|
['name' => $this->name] |
||||||
|
); |
||||||
|
|
||||||
|
session()->flash('message', 'Role saved.'); |
||||||
|
$this->dispatch('roleSaved'); |
||||||
|
} |
||||||
|
|
||||||
|
protected function computeTitle(): string |
||||||
|
{ |
||||||
|
if ($this->mode === 'index') { |
||||||
|
return 'Roles'; |
||||||
|
} |
||||||
|
|
||||||
|
if ($this->mode === 'form' && $this->editingId) { |
||||||
|
return "Edit Role: {$this->name}"; |
||||||
|
} |
||||||
|
|
||||||
|
return 'Create Role'; |
||||||
|
} |
||||||
|
|
||||||
|
public function getTitleProperty(): string |
||||||
|
{ |
||||||
|
return $this->computeTitle(); |
||||||
|
} |
||||||
|
|
||||||
|
public function render() |
||||||
|
{ |
||||||
|
$roles = Role::query() |
||||||
|
->when($this->search, fn($q) => $q->where('name', 'like', "%{$this->search}%")) |
||||||
|
->orderByDesc('id') |
||||||
|
->paginate($this->perPage); |
||||||
|
|
||||||
|
return view('components.role.manager', [ |
||||||
|
'roles' => $roles, |
||||||
|
'title' => $this->title, |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
<?php |
||||||
|
namespace App\Http\Controllers; |
||||||
|
|
||||||
|
use App\Http\Controllers\Controller; |
||||||
|
use Spatie\Permission\Models\Role; |
||||||
|
|
||||||
|
class RoleController extends Controller |
||||||
|
{ |
||||||
|
public function manager() |
||||||
|
{ |
||||||
|
return view('admin.roles.manager'); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
@extends('layouts.app') |
||||||
|
|
||||||
|
@section('title', 'Permissions') |
||||||
|
|
||||||
|
@section('content') |
||||||
|
<livewire:role.manager /> |
||||||
|
@endsection |
@ -0,0 +1,175 @@ |
|||||||
|
{{-- resources/views/livewire/role-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"> |
||||||
|
|
||||||
|
{{-- 1. Dropdown chọn số bản ghi --}} |
||||||
|
<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> |
||||||
|
|
||||||
|
{{-- 2. Search box --}} |
||||||
|
<div class="input-group input-group-outline me-2"> |
||||||
|
<input |
||||||
|
type="text" |
||||||
|
wire:model.debounce.500ms="search" |
||||||
|
wire:keydown.enter="resetPage" |
||||||
|
class="form-control" |
||||||
|
placeholder="Search..." |
||||||
|
> |
||||||
|
</div> |
||||||
|
<button |
||||||
|
wire:click="resetPage" |
||||||
|
class="btn btn-sm btn-outline-success me-2 m-0" |
||||||
|
> |
||||||
|
<i class="fas fa-search me-1" style="font-size:1.2rem;"></i> |
||||||
|
</button> |
||||||
|
|
||||||
|
{{-- 3. Create button --}} |
||||||
|
<button |
||||||
|
wire:click="showForm" |
||||||
|
class="btn btn-sm btn-info m-0" |
||||||
|
> |
||||||
|
Create |
||||||
|
</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 TABLE --}} |
||||||
|
<x-skeleton-table :columns="3" :rows="5" height="3.5rem" /> |
||||||
|
|
||||||
|
{{-- ACTUAL TABLE --}} |
||||||
|
<div wire:loading.remove class="table-responsive w-100"> |
||||||
|
<table class="table align-items-center mb-0 w-100"> |
||||||
|
<thead> |
||||||
|
<tr style="height:3.5rem"> |
||||||
|
<th class="text-center">ID</th> |
||||||
|
<th>Name</th> |
||||||
|
<th class="text-center">Actions</th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
@forelse($roles as $r) |
||||||
|
<tr style="height:3.5rem"> |
||||||
|
<td class="text-center align-middle">{{ $r->id }}</td> |
||||||
|
<td class="align-middle">{{ $r->name }}</td> |
||||||
|
<td class="text-center align-middle"> |
||||||
|
<button |
||||||
|
wire:click="showForm({{ $r->id }})" |
||||||
|
class="btn btn-sm btn-outline-info me-1" |
||||||
|
> |
||||||
|
Edit |
||||||
|
</button> |
||||||
|
<button |
||||||
|
x-data |
||||||
|
@click.prevent=" |
||||||
|
if (confirm('Bạn có chắc chắn muốn xóa role này?')) { |
||||||
|
$wire.delete({{ $r->id }}) |
||||||
|
} |
||||||
|
" |
||||||
|
class="btn btn-sm btn-danger" |
||||||
|
> |
||||||
|
Delete |
||||||
|
</button> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
@empty |
||||||
|
<tr> |
||||||
|
<td colspan="3" class="text-center">No roles found.</td> |
||||||
|
</tr> |
||||||
|
@endforelse |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
|
||||||
|
{{-- Pagination --}} |
||||||
|
<div wire:loading.remove class="mt-3"> |
||||||
|
@if ($roles->lastPage() > 1) |
||||||
|
{{ $roles->links() }} |
||||||
|
@endif |
||||||
|
</div> |
||||||
|
@else |
||||||
|
{{-- SKELETON FORM --}} |
||||||
|
<x-skeleton-form :fields="1" :button-count="2" /> |
||||||
|
|
||||||
|
{{-- ACTUAL FORM --}} |
||||||
|
<div wire:loading.remove x-data> |
||||||
|
<form> |
||||||
|
<div class="mb-3"> |
||||||
|
<label class="form-label">Name</label> |
||||||
|
<input |
||||||
|
type="text" |
||||||
|
wire:model.defer="name" |
||||||
|
class="form-control" |
||||||
|
placeholder="Enter role name" |
||||||
|
> |
||||||
|
@error('name') |
||||||
|
<div class="text-danger text-xs mt-1">{{ $message }}</div> |
||||||
|
@enderror |
||||||
|
</div> |
||||||
|
<div class="d-flex justify-content-end"> |
||||||
|
<button |
||||||
|
wire:click="showIndex" |
||||||
|
type="button" |
||||||
|
class="btn btn-secondary btn-sm me-2" |
||||||
|
> |
||||||
|
Cancel |
||||||
|
</button> |
||||||
|
<button |
||||||
|
type="button" |
||||||
|
@click.prevent=" |
||||||
|
if (confirm('Bạn có chắc chắn muốn {{ $editingId ? 'cập nhật' : 'tạo' }} role này?')) { |
||||||
|
$wire.save() |
||||||
|
} |
||||||
|
" |
||||||
|
class="btn btn-info btn-sm" |
||||||
|
> |
||||||
|
{{ $editingId ? 'Update' : 'Create' }} |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
@endif |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
@script |
||||||
|
<script> |
||||||
|
document.title = @json($title); |
||||||
|
</script> |
||||||
|
@endscript |
||||||
|
</div> |
@ -1,14 +1,21 @@ |
|||||||
<?php |
<?php |
||||||
|
|
||||||
use App\Http\Controllers\Auth\LoginController; |
|
||||||
use Illuminate\Support\Facades\Route; |
use Illuminate\Support\Facades\Route; |
||||||
|
use App\Http\Controllers\Auth\LoginController; |
||||||
use App\Http\Controllers\DashboardController; |
use App\Http\Controllers\DashboardController; |
||||||
use App\Http\Controllers\PermissionController; |
use App\Http\Controllers\PermissionController; |
||||||
use App\Components\Permission\Manager as PermissionManager; |
use App\Http\Controllers\RoleController; |
||||||
|
|
||||||
|
Route::get('dashboard', [DashboardController::class, 'index']) |
||||||
|
->name('dashboard') |
||||||
|
->middleware('auth'); |
||||||
|
|
||||||
// Dashboard route (Livewire) for authenticated users only |
|
||||||
Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard'); |
|
||||||
Route::middleware('auth')->group(function () { |
Route::middleware('auth')->group(function () { |
||||||
|
// Permissions |
||||||
Route::get('/permissions', [PermissionController::class, 'manager']) |
Route::get('/permissions', [PermissionController::class, 'manager']) |
||||||
->name('permissions.index'); |
->name('permissions.index'); |
||||||
|
|
||||||
|
// Roles |
||||||
|
Route::get('/roles', [RoleController::class, 'manager']) |
||||||
|
->name('roles.index'); |
||||||
}); |
}); |
||||||
|
Loading…
Reference in new issue