CRUD Example: Permission Groups
Copy-paste ready Permission Groups CRUD: group controller, views, routes, and JavaScript helpers to organize permissions.
Introduction
Complete Permission Groups management for organizing permissions into logical categories.
Controller: app/Http/Controllers/Admin/GroupController.php
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use RbacSuite\OmniAccess\Models\Group;
use RbacSuite\OmniAccess\Models\Permission;
use RbacSuite\OmniAccess\Services\CacheService;
class GroupController extends Controller
{
protected $cacheService;
public function __construct(CacheService $cacheService)
{
$this->cacheService = $cacheService;
// Apply middleware
$this->middleware('auth');
$this->middleware('permission:groups.view')->only(['index', 'show']);
$this->middleware('permission:groups.create')->only(['create', 'store']);
$this->middleware('permission:groups.edit')->only(['edit', 'update', 'reorder']);
$this->middleware('permission:groups.delete')->only('destroy');
}
/**
* Display a listing of groups
*/
public function index(Request $request)
{
$query = Group::withCount('permissions');
// Search functionality
if ($request->filled('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('slug', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
}
// Sort functionality
$sortField = $request->get('sort', 'sort_order');
$sortDirection = $request->get('direction', 'asc');
if (in_array($sortField, ['name', 'slug', 'sort_order', 'permissions_count', 'created_at'])) {
$query->orderBy($sortField, $sortDirection);
}
$groups = $query->paginate(10)->withQueryString();
return view('admin.groups.index', compact('groups'));
}
/**
* Show the form for creating a new group
*/
public function create()
{
$maxSortOrder = Group::max('sort_order') ?? 0;
$suggestedSortOrder = $maxSortOrder + 10;
return view('admin.groups.create', compact('suggestedSortOrder'));
}
/**
* Store a newly created group
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:50',
'slug' => 'required|string|max:50|unique:permission_groups,slug|regex:/^[a-z0-9\-]+$/',
'description' => 'nullable|string|max:255',
'sort_order' => 'nullable|integer|min:0'
]);
// Create group
$group = Group::create([
'name' => $validated['name'],
'slug' => $validated['slug'],
'description' => $validated['description'],
'sort_order' => $validated['sort_order'] ?? 0
]);
// Clear cache
$this->cacheService->forget('groups.all');
if ($request->input('create_another')) {
return redirect()
->route('admin.groups.create')
->with('success', 'Group created successfully! You can add another.');
}
return redirect()
->route('admin.groups.index')
->with('success', 'Group created successfully!');
}
/**
* Display the specified group
*/
public function show(Group $group)
{
$group->load(['permissions' => function ($query) {
$query->withCount('roles');
}]);
return view('admin.groups.show', compact('group'));
}
/**
* Show the form for editing the specified group
*/
public function edit(Group $group)
{
return view('admin.groups.edit', compact('group'));
}
/**
* Update the specified group
*/
public function update(Request $request, Group $group)
{
$validated = $request->validate([
'name' => 'required|string|max:50',
'slug' => 'required|string|max:50|unique:permission_groups,slug,' . $group->id . '|regex:/^[a-z0-9\-]+$/',
'description' => 'nullable|string|max:255',
'sort_order' => 'nullable|integer|min:0'
]);
// Update group
$group->update([
'name' => $validated['name'],
'slug' => $validated['slug'],
'description' => $validated['description'],
'sort_order' => $validated['sort_order'] ?? 0
]);
// Clear cache
$this->cacheService->forget('groups.all');
if ($request->input('save_and_continue')) {
return redirect()
->route('admin.groups.edit', $group)
->with('success', 'Group updated successfully!');
}
return redirect()
->route('admin.groups.show', $group)
->with('success', 'Group updated successfully!');
}
/**
* Remove the specified group
*/
public function destroy(Group $group)
{
// Check if group has permissions
if ($group->permissions()->count() > 0) {
return response()->json([
'success' => false,
'message' => 'Cannot delete group with permissions! Move permissions to another group first.'
], 403);
}
$group->delete();
// Clear cache
$this->cacheService->forget('groups.all');
if (request()->wantsJson()) {
return response()->json([
'success' => true,
'message' => 'Group deleted successfully!'
]);
}
return redirect()
->route('admin.groups.index')
->with('success', 'Group deleted successfully!');
}
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use RbacSuite\OmniAccess\Models\Group;
use RbacSuite\OmniAccess\Models\Permission;
use RbacSuite\OmniAccess\Services\CacheService;
class GroupController extends Controller
{
protected $cacheService;
public function __construct(CacheService $cacheService)
{
$this->cacheService = $cacheService;
// Apply middleware
$this->middleware('auth');
$this->middleware('permission:groups.view')->only(['index', 'show']);
$this->middleware('permission:groups.create')->only(['create', 'store']);
$this->middleware('permission:groups.edit')->only(['edit', 'update', 'reorder']);
$this->middleware('permission:groups.delete')->only('destroy');
}
/**
* Display a listing of groups
*/
public function index(Request $request)
{
$query = Group::withCount('permissions');
// Search functionality
if ($request->filled('search')) {
$search = $request->get('search');
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('slug', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
}
// Sort functionality
$sortField = $request->get('sort', 'sort_order');
$sortDirection = $request->get('direction', 'asc');
if (in_array($sortField, ['name', 'slug', 'sort_order', 'permissions_count', 'created_at'])) {
$query->orderBy($sortField, $sortDirection);
}
$groups = $query->paginate(10)->withQueryString();
return view('admin.groups.index', compact('groups'));
}
/**
* Show the form for creating a new group
*/
public function create()
{
$maxSortOrder = Group::max('sort_order') ?? 0;
$suggestedSortOrder = $maxSortOrder + 10;
return view('admin.groups.create', compact('suggestedSortOrder'));
}
/**
* Store a newly created group
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:50',
'slug' => 'required|string|max:50|unique:permission_groups,slug|regex:/^[a-z0-9\-]+$/',
'description' => 'nullable|string|max:255',
'sort_order' => 'nullable|integer|min:0'
]);
// Create group
$group = Group::create([
'name' => $validated['name'],
'slug' => $validated['slug'],
'description' => $validated['description'],
'sort_order' => $validated['sort_order'] ?? 0
]);
// Clear cache
$this->cacheService->forget('groups.all');
if ($request->input('create_another')) {
return redirect()
->route('admin.groups.create')
->with('success', 'Group created successfully! You can add another.');
}
return redirect()
->route('admin.groups.index')
->with('success', 'Group created successfully!');
}
/**
* Display the specified group
*/
public function show(Group $group)
{
$group->load(['permissions' => function ($query) {
$query->withCount('roles');
}]);
return view('admin.groups.show', compact('group'));
}
/**
* Show the form for editing the specified group
*/
public function edit(Group $group)
{
return view('admin.groups.edit', compact('group'));
}
/**
* Update the specified group
*/
public function update(Request $request, Group $group)
{
$validated = $request->validate([
'name' => 'required|string|max:50',
'slug' => 'required|string|max:50|unique:permission_groups,slug,' . $group->id . '|regex:/^[a-z0-9\-]+$/',
'description' => 'nullable|string|max:255',
'sort_order' => 'nullable|integer|min:0'
]);
// Update group
$group->update([
'name' => $validated['name'],
'slug' => $validated['slug'],
'description' => $validated['description'],
'sort_order' => $validated['sort_order'] ?? 0
]);
// Clear cache
$this->cacheService->forget('groups.all');
if ($request->input('save_and_continue')) {
return redirect()
->route('admin.groups.edit', $group)
->with('success', 'Group updated successfully!');
}
return redirect()
->route('admin.groups.show', $group)
->with('success', 'Group updated successfully!');
}
/**
* Remove the specified group
*/
public function destroy(Group $group)
{
// Check if group has permissions
if ($group->permissions()->count() > 0) {
return response()->json([
'success' => false,
'message' => 'Cannot delete group with permissions! Move permissions to another group first.'
], 403);
}
$group->delete();
// Clear cache
$this->cacheService->forget('groups.all');
if (request()->wantsJson()) {
return response()->json([
'success' => true,
'message' => 'Group deleted successfully!'
]);
}
return redirect()
->route('admin.groups.index')
->with('success', 'Group deleted successfully!');
}
View: resources/views/admin/groups/index.blade.php
@extends('layouts.admin')
@section('title', 'Permission Groups Management')
@section('content')
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="page-header d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0 text-gray-800">Permission Groups</h1>
<p class="text-muted mb-0">Organize permissions into logical categories</p>
</div>
<div>
@permission('groups.create')
<a href="{{ route('admin.groups.create') }}" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>Create New Group
</a>
@endpermission
</div>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-left-primary shadow-sm h-100">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Total Groups
</div>
<div class="h5 mb-0 font-weight-bold">{{ \RbacSuite\OmniAccess\Models\Group::count() }}</div>
</div>
<div class="col-auto">
<i class="bi bi-collection fs-2 text-primary"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-left-success shadow-sm h-100">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Total Permissions
</div>
<div class="h5 mb-0 font-weight-bold">{{ \RbacSuite\OmniAccess\Models\Permission::count() }}</div>
</div>
<div class="col-auto">
<i class="bi bi-key fs-2 text-success"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-left-info shadow-sm h-100">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Grouped Permissions
</div>
<div class="h5 mb-0 font-weight-bold">
{{ \RbacSuite\OmniAccess\Models\Permission::whereNotNull('group_id')->count() }}
</div>
</div>
<div class="col-auto">
<i class="bi bi-diagram-3 fs-2 text-info"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-left-warning shadow-sm h-100">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Ungrouped
</div>
<div class="h5 mb-0 font-weight-bold">
{{ \RbacSuite\OmniAccess\Models\Permission::whereNull('group_id')->count() }}
</div>
</div>
<div class="col-auto">
<i class="bi bi-exclamation-circle fs-2 text-warning"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Alerts -->
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
<!-- Main Card -->
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<div class="row align-items-center">
<div class="col-md-6">
<form action="{{ route('admin.groups.index') }}" method="GET" class="d-flex">
<div class="input-group">
<input type="text"
name="search"
class="form-control"
placeholder="Search groups..."
value="{{ request('search') }}">
<button class="btn btn-outline-secondary" type="submit">
<i class="bi bi-search"></i>
</button>
@if(request('search'))
<a href="{{ route('admin.groups.index') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-lg"></i>
</a>
@endif
</div>
</form>
</div>
<div class="col-md-6 text-end">
@permission('groups.edit')
<button type="button" class="btn btn-outline-primary" onclick="enableReorder()" id="reorderBtn">
<i class="bi bi-arrows-move me-1"></i>Reorder
</button>
<button type="button" class="btn btn-success d-none" onclick="saveOrder()" id="saveOrderBtn">
<i class="bi bi-save me-1"></i>Save Order
</button>
<button type="button" class="btn btn-secondary d-none" onclick="cancelReorder()" id="cancelOrderBtn">
<i class="bi bi-x me-1"></i>Cancel
</button>
@endpermission
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="groupsTable">
<thead class="table-light">
<tr>
<th class="reorder-handle d-none" style="width: 40px;"></th>
<th>
<a href="{{ route('admin.groups.index', ['sort' => 'sort_order', 'direction' => request('direction') == 'asc' ? 'desc' : 'asc'] + request()->only('search')) }}"
class="text-decoration-none text-dark">
Order
@if(request('sort') == 'sort_order')
<i class="bi bi-chevron-{{ request('direction') == 'asc' ? 'up' : 'down' }} ms-1"></i>
@endif
</a>
</th>
<th>Group Name</th>
<th>Slug</th>
<th>Description</th>
<th class="text-center">
<a href="{{ route('admin.groups.index', ['sort' => 'permissions_count', 'direction' => request('direction') == 'asc' ? 'desc' : 'asc'] + request()->only('search')) }}"
class="text-decoration-none text-dark">
Permissions
@if(request('sort') == 'permissions_count')
<i class="bi bi-chevron-{{ request('direction') == 'asc' ? 'up' : 'down' }} ms-1"></i>
@endif
</a>
</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@forelse($groups as $group)
<tr data-id="{{ $group->id }}" data-order="{{ $group->sort_order }}">
<td class="reorder-handle d-none">
<i class="bi bi-grip-vertical text-muted"></i>
</td>
<td>
<span class="badge bg-secondary">{{ $group->sort_order }}</span>
</td>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle me-2">
<i class="bi bi-collection text-primary"></i>
</div>
<div>
<div class="fw-semibold">{{ $group->name }}</div>
</div>
</div>
</td>
<td>
<code class="text-primary">{{ $group->slug }}</code>
</td>
<td>
<span class="text-muted small">{{ Str::limit($group->description, 50) ?: '-' }}</span>
</td>
<td class="text-center">
@if($group->permissions_count > 0)
<span class="badge bg-success">{{ $group->permissions_count }}</span>
@else
<span class="badge bg-secondary">0</span>
@endif
</td>
<td class="text-end">
<div class="btn-group" role="group">
<a href="{{ route('admin.groups.show', $group) }}"
class="btn btn-sm btn-outline-info"
title="View">
<i class="bi bi-eye"></i>
</a>
@permission('groups.edit')
<a href="{{ route('admin.groups.edit', $group) }}"
class="btn btn-sm btn-outline-primary"
title="Edit">
<i class="bi bi-pencil"></i>
</a>
@endpermission
@permission('groups.delete')
@if($group->permissions_count == 0)
<button type="button"
class="btn btn-sm btn-outline-danger"
onclick="deleteGroup({{ $group->id }}, '{{ $group->name }}')"
title="Delete">
<i class="bi bi-trash"></i>
</button>
@else
<button type="button"
class="btn btn-sm btn-outline-danger"
disabled
title="Has permissions">
<i class="bi bi-trash"></i>
</button>
@endif
@endpermission
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center py-4">
<div class="text-muted">
<i class="bi bi-inbox fs-1"></i>
<p class="mt-2">No groups found</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@if($groups->hasPages())
<div class="card-footer bg-white">
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted">
Showing {{ $groups->firstItem() }} to {{ $groups->lastItem() }} of {{ $groups->total() }} results
</div>
<div>
{{ $groups->links('pagination::bootstrap-5') }}
</div>
</div>
</div>
@endif
</div>
</div>
@endsection
@extends('layouts.admin')
@section('title', 'Permission Groups Management')
@section('content')
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="page-header d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0 text-gray-800">Permission Groups</h1>
<p class="text-muted mb-0">Organize permissions into logical categories</p>
</div>
<div>
@permission('groups.create')
<a href="{{ route('admin.groups.create') }}" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>Create New Group
</a>
@endpermission
</div>
</div>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-left-primary shadow-sm h-100">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
Total Groups
</div>
<div class="h5 mb-0 font-weight-bold">{{ \RbacSuite\OmniAccess\Models\Group::count() }}</div>
</div>
<div class="col-auto">
<i class="bi bi-collection fs-2 text-primary"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-left-success shadow-sm h-100">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Total Permissions
</div>
<div class="h5 mb-0 font-weight-bold">{{ \RbacSuite\OmniAccess\Models\Permission::count() }}</div>
</div>
<div class="col-auto">
<i class="bi bi-key fs-2 text-success"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-left-info shadow-sm h-100">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Grouped Permissions
</div>
<div class="h5 mb-0 font-weight-bold">
{{ \RbacSuite\OmniAccess\Models\Permission::whereNotNull('group_id')->count() }}
</div>
</div>
<div class="col-auto">
<i class="bi bi-diagram-3 fs-2 text-info"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card border-left-warning shadow-sm h-100">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Ungrouped
</div>
<div class="h5 mb-0 font-weight-bold">
{{ \RbacSuite\OmniAccess\Models\Permission::whereNull('group_id')->count() }}
</div>
</div>
<div class="col-auto">
<i class="bi bi-exclamation-circle fs-2 text-warning"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Alerts -->
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
@endif
<!-- Main Card -->
<div class="card shadow-sm">
<div class="card-header bg-white py-3">
<div class="row align-items-center">
<div class="col-md-6">
<form action="{{ route('admin.groups.index') }}" method="GET" class="d-flex">
<div class="input-group">
<input type="text"
name="search"
class="form-control"
placeholder="Search groups..."
value="{{ request('search') }}">
<button class="btn btn-outline-secondary" type="submit">
<i class="bi bi-search"></i>
</button>
@if(request('search'))
<a href="{{ route('admin.groups.index') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-lg"></i>
</a>
@endif
</div>
</form>
</div>
<div class="col-md-6 text-end">
@permission('groups.edit')
<button type="button" class="btn btn-outline-primary" onclick="enableReorder()" id="reorderBtn">
<i class="bi bi-arrows-move me-1"></i>Reorder
</button>
<button type="button" class="btn btn-success d-none" onclick="saveOrder()" id="saveOrderBtn">
<i class="bi bi-save me-1"></i>Save Order
</button>
<button type="button" class="btn btn-secondary d-none" onclick="cancelReorder()" id="cancelOrderBtn">
<i class="bi bi-x me-1"></i>Cancel
</button>
@endpermission
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="groupsTable">
<thead class="table-light">
<tr>
<th class="reorder-handle d-none" style="width: 40px;"></th>
<th>
<a href="{{ route('admin.groups.index', ['sort' => 'sort_order', 'direction' => request('direction') == 'asc' ? 'desc' : 'asc'] + request()->only('search')) }}"
class="text-decoration-none text-dark">
Order
@if(request('sort') == 'sort_order')
<i class="bi bi-chevron-{{ request('direction') == 'asc' ? 'up' : 'down' }} ms-1"></i>
@endif
</a>
</th>
<th>Group Name</th>
<th>Slug</th>
<th>Description</th>
<th class="text-center">
<a href="{{ route('admin.groups.index', ['sort' => 'permissions_count', 'direction' => request('direction') == 'asc' ? 'desc' : 'asc'] + request()->only('search')) }}"
class="text-decoration-none text-dark">
Permissions
@if(request('sort') == 'permissions_count')
<i class="bi bi-chevron-{{ request('direction') == 'asc' ? 'up' : 'down' }} ms-1"></i>
@endif
</a>
</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@forelse($groups as $group)
<tr data-id="{{ $group->id }}" data-order="{{ $group->sort_order }}">
<td class="reorder-handle d-none">
<i class="bi bi-grip-vertical text-muted"></i>
</td>
<td>
<span class="badge bg-secondary">{{ $group->sort_order }}</span>
</td>
<td>
<div class="d-flex align-items-center">
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle me-2">
<i class="bi bi-collection text-primary"></i>
</div>
<div>
<div class="fw-semibold">{{ $group->name }}</div>
</div>
</div>
</td>
<td>
<code class="text-primary">{{ $group->slug }}</code>
</td>
<td>
<span class="text-muted small">{{ Str::limit($group->description, 50) ?: '-' }}</span>
</td>
<td class="text-center">
@if($group->permissions_count > 0)
<span class="badge bg-success">{{ $group->permissions_count }}</span>
@else
<span class="badge bg-secondary">0</span>
@endif
</td>
<td class="text-end">
<div class="btn-group" role="group">
<a href="{{ route('admin.groups.show', $group) }}"
class="btn btn-sm btn-outline-info"
title="View">
<i class="bi bi-eye"></i>
</a>
@permission('groups.edit')
<a href="{{ route('admin.groups.edit', $group) }}"
class="btn btn-sm btn-outline-primary"
title="Edit">
<i class="bi bi-pencil"></i>
</a>
@endpermission
@permission('groups.delete')
@if($group->permissions_count == 0)
<button type="button"
class="btn btn-sm btn-outline-danger"
onclick="deleteGroup({{ $group->id }}, '{{ $group->name }}')"
title="Delete">
<i class="bi bi-trash"></i>
</button>
@else
<button type="button"
class="btn btn-sm btn-outline-danger"
disabled
title="Has permissions">
<i class="bi bi-trash"></i>
</button>
@endif
@endpermission
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="text-center py-4">
<div class="text-muted">
<i class="bi bi-inbox fs-1"></i>
<p class="mt-2">No groups found</p>
</div>
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
@if($groups->hasPages())
<div class="card-footer bg-white">
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted">
Showing {{ $groups->firstItem() }} to {{ $groups->lastItem() }} of {{ $groups->total() }} results
</div>
<div>
{{ $groups->links('pagination::bootstrap-5') }}
</div>
</div>
</div>
@endif
</div>
</div>
@endsection
View: resources/views/admin/groups/create.blade.php
@extends('layouts.admin')
@section('title', 'Create Permission Group')
@section('content')
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.groups.index') }}">Permission Groups</a></li>
<li class="breadcrumb-item active">Create Group</li>
</ol>
</nav>
<h1 class="h3 mb-0 text-gray-800">Create New Permission Group</h1>
</div>
</div>
<form action="{{ route('admin.groups.store') }}" method="POST" id="createGroupForm">
@csrf
<div class="row">
<!-- Group Information -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Group Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="name" class="form-label">Group Name <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
id="name"
name="name"
value="{{ old('name') }}"
placeholder="e.g., User Management"
required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="form-text text-muted">Display name for the permission group</small>
</div>
<div class="mb-3">
<label for="slug" class="form-label">Slug <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('slug') is-invalid @enderror"
id="slug"
name="slug"
value="{{ old('slug') }}"
placeholder="e.g., user-management"
pattern="[a-z0-9\-]+"
required>
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="form-text text-muted">Unique identifier (lowercase, hyphens only)</small>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control @error('description') is-invalid @enderror"
id="description"
name="description"
rows="3"
placeholder="Brief description of what permissions in this group control...">{{ old('description') }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="sort_order" class="form-label">Sort Order</label>
<input type="number"
class="form-control @error('sort_order') is-invalid @enderror"
id="sort_order"
name="sort_order"
value="{{ old('sort_order', $suggestedSortOrder) }}"
min="0"
step="10">
@error('sort_order')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="form-text text-muted">Lower numbers appear first (suggested: {{ $suggestedSortOrder }})</small>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Quick Actions -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-lightning me-2"></i>Quick Actions</h5>
</div>
<div class="card-body">
<button type="submit" class="btn btn-primary w-100 mb-2">
<i class="bi bi-check-circle me-2"></i>Create Group
</button>
<button type="submit" name="create_another" value="1" class="btn btn-outline-primary w-100 mb-2">
<i class="bi bi-plus-circle me-2"></i>Create & Add Another
</button>
<a href="{{ route('admin.groups.index') }}" class="btn btn-outline-secondary w-100">
<i class="bi bi-x-circle me-2"></i>Cancel
</a>
</div>
</div>
<!-- Common Group Examples -->
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-lightbulb me-2"></i>Common Groups</h5>
</div>
<div class="card-body">
<p class="small text-muted mb-3">Click to use these common group patterns:</p>
<div class="d-grid gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary text-start"
onclick="fillGroup('User Management', 'user-management', 'Permissions for managing users and roles')">
<i class="bi bi-people me-2"></i>User Management
</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-start"
onclick="fillGroup('Content Management', 'content-management', 'Permissions for managing content')">
<i class="bi bi-file-text me-2"></i>Content Management
</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-start"
onclick="fillGroup('System Settings', 'system-settings', 'Permissions for system configuration')">
<i class="bi bi-gear me-2"></i>System Settings
</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-start"
onclick="fillGroup('Reports', 'reports', 'Permissions for viewing and generating reports')">
<i class="bi bi-graph-up me-2"></i>Reports
</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-start"
onclick="fillGroup('API Access', 'api-access', 'Permissions for API endpoints')">
<i class="bi bi-plug me-2"></i>API Access
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
@endsection
@extends('layouts.admin')
@section('title', 'Create Permission Group')
@section('content')
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.groups.index') }}">Permission Groups</a></li>
<li class="breadcrumb-item active">Create Group</li>
</ol>
</nav>
<h1 class="h3 mb-0 text-gray-800">Create New Permission Group</h1>
</div>
</div>
<form action="{{ route('admin.groups.store') }}" method="POST" id="createGroupForm">
@csrf
<div class="row">
<!-- Group Information -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Group Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="name" class="form-label">Group Name <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
id="name"
name="name"
value="{{ old('name') }}"
placeholder="e.g., User Management"
required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="form-text text-muted">Display name for the permission group</small>
</div>
<div class="mb-3">
<label for="slug" class="form-label">Slug <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('slug') is-invalid @enderror"
id="slug"
name="slug"
value="{{ old('slug') }}"
placeholder="e.g., user-management"
pattern="[a-z0-9\-]+"
required>
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="form-text text-muted">Unique identifier (lowercase, hyphens only)</small>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control @error('description') is-invalid @enderror"
id="description"
name="description"
rows="3"
placeholder="Brief description of what permissions in this group control...">{{ old('description') }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="sort_order" class="form-label">Sort Order</label>
<input type="number"
class="form-control @error('sort_order') is-invalid @enderror"
id="sort_order"
name="sort_order"
value="{{ old('sort_order', $suggestedSortOrder) }}"
min="0"
step="10">
@error('sort_order')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="form-text text-muted">Lower numbers appear first (suggested: {{ $suggestedSortOrder }})</small>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Quick Actions -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-lightning me-2"></i>Quick Actions</h5>
</div>
<div class="card-body">
<button type="submit" class="btn btn-primary w-100 mb-2">
<i class="bi bi-check-circle me-2"></i>Create Group
</button>
<button type="submit" name="create_another" value="1" class="btn btn-outline-primary w-100 mb-2">
<i class="bi bi-plus-circle me-2"></i>Create & Add Another
</button>
<a href="{{ route('admin.groups.index') }}" class="btn btn-outline-secondary w-100">
<i class="bi bi-x-circle me-2"></i>Cancel
</a>
</div>
</div>
<!-- Common Group Examples -->
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-lightbulb me-2"></i>Common Groups</h5>
</div>
<div class="card-body">
<p class="small text-muted mb-3">Click to use these common group patterns:</p>
<div class="d-grid gap-2">
<button type="button" class="btn btn-sm btn-outline-secondary text-start"
onclick="fillGroup('User Management', 'user-management', 'Permissions for managing users and roles')">
<i class="bi bi-people me-2"></i>User Management
</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-start"
onclick="fillGroup('Content Management', 'content-management', 'Permissions for managing content')">
<i class="bi bi-file-text me-2"></i>Content Management
</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-start"
onclick="fillGroup('System Settings', 'system-settings', 'Permissions for system configuration')">
<i class="bi bi-gear me-2"></i>System Settings
</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-start"
onclick="fillGroup('Reports', 'reports', 'Permissions for viewing and generating reports')">
<i class="bi bi-graph-up me-2"></i>Reports
</button>
<button type="button" class="btn btn-sm btn-outline-secondary text-start"
onclick="fillGroup('API Access', 'api-access', 'Permissions for API endpoints')">
<i class="bi bi-plug me-2"></i>API Access
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
@endsection
View: resources/views/admin/groups/edit.blade.php
@extends('layouts.admin')
@section('title', 'Edit Group - ' . $group->name)
@section('content')
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.groups.index') }}">Permission Groups</a></li>
<li class="breadcrumb-item active">Edit Group</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="h3 mb-0 text-gray-800">Edit Group: {{ $group->name }}</h1>
<a href="{{ route('admin.groups.show', $group) }}" class="btn btn-outline-info">
<i class="bi bi-eye me-2"></i>View Group
</a>
</div>
</div>
</div>
<form action="{{ route('admin.groups.update', $group) }}" method="POST">
@csrf
@method('PUT')
<div class="row">
<!-- Group Information -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Group Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="name" class="form-label">Group Name <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
id="name"
name="name"
value="{{ old('name', $group->name) }}"
required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="slug" class="form-label">Slug <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('slug') is-invalid @enderror"
id="slug"
name="slug"
value="{{ old('slug', $group->slug) }}"
pattern="[a-z0-9\-]+"
required>
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="form-text text-muted">⚠️ Changing the slug may affect code references</small>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control @error('description') is-invalid @enderror"
id="description"
name="description"
rows="3">{{ old('description', $group->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="sort_order" class="form-label">Sort Order</label>
<input type="number"
class="form-control @error('sort_order') is-invalid @enderror"
id="sort_order"
name="sort_order"
value="{{ old('sort_order', $group->sort_order) }}"
min="0"
step="10">
@error('sort_order')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="form-text text-muted">Lower numbers appear first</small>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Actions -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-lightning me-2"></i>Actions</h5>
</div>
<div class="card-body">
<button type="submit" class="btn btn-primary w-100 mb-2">
<i class="bi bi-save me-2"></i>Save Changes
</button>
<button type="submit" name="save_and_continue" value="1" class="btn btn-outline-primary w-100 mb-2">
<i class="bi bi-save me-2"></i>Save & Continue Editing
</button>
<a href="{{ route('admin.groups.index') }}" class="btn btn-outline-secondary w-100">
<i class="bi bi-x-circle me-2"></i>Cancel
</a>
</div>
</div>
<!-- Group Details -->
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Group Details</h5>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-5">Created:</dt>
<dd class="col-7">{{ $group->created_at->format('M d, Y') }}</dd>
<dt class="col-5">Updated:</dt>
<dd class="col-7">{{ $group->updated_at->format('M d, Y') }}</dd>
<dt class="col-5">Permissions:</dt>
<dd class="col-7">
<span class="badge bg-success">{{ $group->permissions()->count() }}</span>
</dd>
</dl>
</div>
</div>
</div>
</div>
</form>
</div>
@endsection
@extends('layouts.admin')
@section('title', 'Edit Group - ' . $group->name)
@section('content')
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.groups.index') }}">Permission Groups</a></li>
<li class="breadcrumb-item active">Edit Group</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="h3 mb-0 text-gray-800">Edit Group: {{ $group->name }}</h1>
<a href="{{ route('admin.groups.show', $group) }}" class="btn btn-outline-info">
<i class="bi bi-eye me-2"></i>View Group
</a>
</div>
</div>
</div>
<form action="{{ route('admin.groups.update', $group) }}" method="POST">
@csrf
@method('PUT')
<div class="row">
<!-- Group Information -->
<div class="col-lg-8">
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Group Information</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="name" class="form-label">Group Name <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('name') is-invalid @enderror"
id="name"
name="name"
value="{{ old('name', $group->name) }}"
required>
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="slug" class="form-label">Slug <span class="text-danger">*</span></label>
<input type="text"
class="form-control @error('slug') is-invalid @enderror"
id="slug"
name="slug"
value="{{ old('slug', $group->slug) }}"
pattern="[a-z0-9\-]+"
required>
@error('slug')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="form-text text-muted">⚠️ Changing the slug may affect code references</small>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control @error('description') is-invalid @enderror"
id="description"
name="description"
rows="3">{{ old('description', $group->description) }}</textarea>
@error('description')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="sort_order" class="form-label">Sort Order</label>
<input type="number"
class="form-control @error('sort_order') is-invalid @enderror"
id="sort_order"
name="sort_order"
value="{{ old('sort_order', $group->sort_order) }}"
min="0"
step="10">
@error('sort_order')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
<small class="form-text text-muted">Lower numbers appear first</small>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Actions -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-lightning me-2"></i>Actions</h5>
</div>
<div class="card-body">
<button type="submit" class="btn btn-primary w-100 mb-2">
<i class="bi bi-save me-2"></i>Save Changes
</button>
<button type="submit" name="save_and_continue" value="1" class="btn btn-outline-primary w-100 mb-2">
<i class="bi bi-save me-2"></i>Save & Continue Editing
</button>
<a href="{{ route('admin.groups.index') }}" class="btn btn-outline-secondary w-100">
<i class="bi bi-x-circle me-2"></i>Cancel
</a>
</div>
</div>
<!-- Group Details -->
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Group Details</h5>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-5">Created:</dt>
<dd class="col-7">{{ $group->created_at->format('M d, Y') }}</dd>
<dt class="col-5">Updated:</dt>
<dd class="col-7">{{ $group->updated_at->format('M d, Y') }}</dd>
<dt class="col-5">Permissions:</dt>
<dd class="col-7">
<span class="badge bg-success">{{ $group->permissions()->count() }}</span>
</dd>
</dl>
</div>
</div>
</div>
</div>
</form>
</div>
@endsection
View: resources/views/admin/groups/show.blade.php
@extends('layouts.admin')
@section('title', 'Group Details - ' . $group->name)
@section('content')
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.groups.index') }}">Permission Groups</a></li>
<li class="breadcrumb-item active">{{ $group->name }}</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-2 text-gray-800">{{ $group->name }}</h1>
<code class="fs-6 text-primary">{{ $group->slug }}</code>
</div>
<div>
@permission('groups.edit')
<a href="{{ route('admin.groups.edit', $group) }}" class="btn btn-primary">
<i class="bi bi-pencil me-2"></i>Edit Group
</a>
@endpermission
</div>
</div>
</div>
</div>
<div class="row">
<!-- Group Information -->
<div class="col-lg-4 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Group Information</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">Name:</dt>
<dd class="col-sm-8">{{ $group->name }}</dd>
<dt class="col-sm-4">Slug:</dt>
<dd class="col-sm-8"><code>{{ $group->slug }}</code></dd>
<dt class="col-sm-4">Sort Order:</dt>
<dd class="col-sm-8">
<span class="badge bg-secondary">{{ $group->sort_order }}</span>
</dd>
<dt class="col-sm-4">Description:</dt>
<dd class="col-sm-8">{{ $group->description ?: 'No description' }}</dd>
<dt class="col-sm-4">Created:</dt>
<dd class="col-sm-8">
{{ $group->created_at->format('M d, Y') }}
<small class="text-muted d-block">{{ $group->created_at->diffForHumans() }}</small>
</dd>
<dt class="col-sm-4">Updated:</dt>
<dd class="col-sm-8">
{{ $group->updated_at->format('M d, Y') }}
<small class="text-muted d-block">{{ $group->updated_at->diffForHumans() }}</small>
</dd>
</dl>
<hr>
<div class="text-center">
<h3 class="mb-0">{{ $group->permissions->count() }}</h3>
<p class="text-muted mb-0">Permissions in this group</p>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-lightning me-2"></i>Quick Actions</h5>
</div>
<div class="card-body">
@permission('groups.edit')
<a href="{{ route('admin.groups.edit', $group) }}" class="btn btn-outline-primary w-100 mb-2">
<i class="bi bi-pencil me-2"></i>Edit Group
</a>
@endpermission
@permission('permissions.create')
<a href="{{ route('admin.permissions.create', ['group_id' => $group->id]) }}" class="btn btn-outline-success w-100 mb-2">
<i class="bi bi-plus-circle me-2"></i>Add Permission
</a>
@endpermission
@permission('groups.delete')
@if($group->permissions->isEmpty())
<button type="button"
class="btn btn-outline-danger w-100"
onclick="confirmDelete()">
<i class="bi bi-trash me-2"></i>Delete Group
</button>
@else
<button type="button" class="btn btn-outline-danger w-100" disabled>
<i class="bi bi-lock me-2"></i>Cannot Delete (Has Permissions)
</button>
@endif
@endpermission
</div>
</div>
</div>
<!-- Permissions in Group -->
<div class="col-lg-8 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-key me-2"></i>
Permissions in this Group
<span class="badge bg-primary ms-2">{{ $group->permissions->count() }}</span>
</h5>
@permission('permissions.create')
<a href="{{ route('admin.permissions.create', ['group_id' => $group->id]) }}" class="btn btn-sm btn-primary">
<i class="bi bi-plus-circle me-1"></i>Add Permission
</a>
@endpermission
</div>
<div class="card-body">
@forelse($group->permissions as $permission)
<div class="border-bottom pb-3 mb-3">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">
{{ $permission->name }}
@if($permission->roles_count > 0)
<span class="badge bg-success ms-2">
{{ $permission->roles_count }} roles
</span>
@else
<span class="badge bg-warning ms-2">
Unassigned
</span>
@endif
</h6>
<code class="text-primary">{{ $permission->slug }}</code>
@if($permission->description)
<p class="text-muted small mb-0 mt-1">{{ $permission->description }}</p>
@endif
</div>
<div class="btn-group" role="group">
<a href="{{ route('admin.permissions.show', $permission) }}"
class="btn btn-sm btn-outline-info"
title="View">
<i class="bi bi-eye"></i>
</a>
@permission('permissions.edit')
<a href="{{ route('admin.permissions.edit', $permission) }}"
class="btn btn-sm btn-outline-primary"
title="Edit">
<i class="bi bi-pencil"></i>
</a>
@endpermission
</div>
</div>
</div>
@empty
<div class="text-center py-5">
<i class="bi bi-key-fill fs-1 text-muted"></i>
<p class="mt-2 text-muted">No permissions in this group</p>
@permission('permissions.create')
<a href="{{ route('admin.permissions.create', ['group_id' => $group->id]) }}" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>Add First Permission
</a>
@endpermission
</div>
@endforelse
</div>
</div>
@if($group->permissions->count() > 0)
<!-- Permission Statistics -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-bar-chart me-2"></i>Statistics</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-4">
<h4>{{ $group->permissions->count() }}</h4>
<p class="text-muted mb-0">Total Permissions</p>
</div>
<div class="col-4">
<h4>{{ $group->permissions->sum('roles_count') }}</h4>
<p class="text-muted mb-0">Role Assignments</p>
</div>
<div class="col-4">
<h4>{{ $group->permissions->filter(fn($p) => $p->roles_count === 0)->count() }}</h4>
<p class="text-muted mb-0">Unassigned</p>
</div>
</div>
</div>
</div>
@endif
</div>
</div>
</div>
@push('scripts')
<script>
function confirmDelete() {
if (confirm('Are you sure you want to delete this group? This action cannot be undone.')) {
fetch('{{ route("admin.groups.destroy", $group) }}', {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = '{{ route("admin.groups.index") }}';
} else {
alert(data.message || 'Error deleting group');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred');
});
}
}
</script>
@endpush
@endsection
@extends('layouts.admin')
@section('title', 'Group Details - ' . $group->name)
@section('content')
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.groups.index') }}">Permission Groups</a></li>
<li class="breadcrumb-item active">{{ $group->name }}</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-2 text-gray-800">{{ $group->name }}</h1>
<code class="fs-6 text-primary">{{ $group->slug }}</code>
</div>
<div>
@permission('groups.edit')
<a href="{{ route('admin.groups.edit', $group) }}" class="btn btn-primary">
<i class="bi bi-pencil me-2"></i>Edit Group
</a>
@endpermission
</div>
</div>
</div>
</div>
<div class="row">
<!-- Group Information -->
<div class="col-lg-4 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Group Information</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-4">Name:</dt>
<dd class="col-sm-8">{{ $group->name }}</dd>
<dt class="col-sm-4">Slug:</dt>
<dd class="col-sm-8"><code>{{ $group->slug }}</code></dd>
<dt class="col-sm-4">Sort Order:</dt>
<dd class="col-sm-8">
<span class="badge bg-secondary">{{ $group->sort_order }}</span>
</dd>
<dt class="col-sm-4">Description:</dt>
<dd class="col-sm-8">{{ $group->description ?: 'No description' }}</dd>
<dt class="col-sm-4">Created:</dt>
<dd class="col-sm-8">
{{ $group->created_at->format('M d, Y') }}
<small class="text-muted d-block">{{ $group->created_at->diffForHumans() }}</small>
</dd>
<dt class="col-sm-4">Updated:</dt>
<dd class="col-sm-8">
{{ $group->updated_at->format('M d, Y') }}
<small class="text-muted d-block">{{ $group->updated_at->diffForHumans() }}</small>
</dd>
</dl>
<hr>
<div class="text-center">
<h3 class="mb-0">{{ $group->permissions->count() }}</h3>
<p class="text-muted mb-0">Permissions in this group</p>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-lightning me-2"></i>Quick Actions</h5>
</div>
<div class="card-body">
@permission('groups.edit')
<a href="{{ route('admin.groups.edit', $group) }}" class="btn btn-outline-primary w-100 mb-2">
<i class="bi bi-pencil me-2"></i>Edit Group
</a>
@endpermission
@permission('permissions.create')
<a href="{{ route('admin.permissions.create', ['group_id' => $group->id]) }}" class="btn btn-outline-success w-100 mb-2">
<i class="bi bi-plus-circle me-2"></i>Add Permission
</a>
@endpermission
@permission('groups.delete')
@if($group->permissions->isEmpty())
<button type="button"
class="btn btn-outline-danger w-100"
onclick="confirmDelete()">
<i class="bi bi-trash me-2"></i>Delete Group
</button>
@else
<button type="button" class="btn btn-outline-danger w-100" disabled>
<i class="bi bi-lock me-2"></i>Cannot Delete (Has Permissions)
</button>
@endif
@endpermission
</div>
</div>
</div>
<!-- Permissions in Group -->
<div class="col-lg-8 mb-4">
<div class="card shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-key me-2"></i>
Permissions in this Group
<span class="badge bg-primary ms-2">{{ $group->permissions->count() }}</span>
</h5>
@permission('permissions.create')
<a href="{{ route('admin.permissions.create', ['group_id' => $group->id]) }}" class="btn btn-sm btn-primary">
<i class="bi bi-plus-circle me-1"></i>Add Permission
</a>
@endpermission
</div>
<div class="card-body">
@forelse($group->permissions as $permission)
<div class="border-bottom pb-3 mb-3">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">
{{ $permission->name }}
@if($permission->roles_count > 0)
<span class="badge bg-success ms-2">
{{ $permission->roles_count }} roles
</span>
@else
<span class="badge bg-warning ms-2">
Unassigned
</span>
@endif
</h6>
<code class="text-primary">{{ $permission->slug }}</code>
@if($permission->description)
<p class="text-muted small mb-0 mt-1">{{ $permission->description }}</p>
@endif
</div>
<div class="btn-group" role="group">
<a href="{{ route('admin.permissions.show', $permission) }}"
class="btn btn-sm btn-outline-info"
title="View">
<i class="bi bi-eye"></i>
</a>
@permission('permissions.edit')
<a href="{{ route('admin.permissions.edit', $permission) }}"
class="btn btn-sm btn-outline-primary"
title="Edit">
<i class="bi bi-pencil"></i>
</a>
@endpermission
</div>
</div>
</div>
@empty
<div class="text-center py-5">
<i class="bi bi-key-fill fs-1 text-muted"></i>
<p class="mt-2 text-muted">No permissions in this group</p>
@permission('permissions.create')
<a href="{{ route('admin.permissions.create', ['group_id' => $group->id]) }}" class="btn btn-primary">
<i class="bi bi-plus-circle me-2"></i>Add First Permission
</a>
@endpermission
</div>
@endforelse
</div>
</div>
@if($group->permissions->count() > 0)
<!-- Permission Statistics -->
<div class="card shadow-sm mt-4">
<div class="card-header bg-white">
<h5 class="mb-0"><i class="bi bi-bar-chart me-2"></i>Statistics</h5>
</div>
<div class="card-body">
<div class="row text-center">
<div class="col-4">
<h4>{{ $group->permissions->count() }}</h4>
<p class="text-muted mb-0">Total Permissions</p>
</div>
<div class="col-4">
<h4>{{ $group->permissions->sum('roles_count') }}</h4>
<p class="text-muted mb-0">Role Assignments</p>
</div>
<div class="col-4">
<h4>{{ $group->permissions->filter(fn($p) => $p->roles_count === 0)->count() }}</h4>
<p class="text-muted mb-0">Unassigned</p>
</div>
</div>
</div>
</div>
@endif
</div>
</div>
</div>
@push('scripts')
<script>
function confirmDelete() {
if (confirm('Are you sure you want to delete this group? This action cannot be undone.')) {
fetch('{{ route("admin.groups.destroy", $group) }}', {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': '{{ csrf_token() }}',
'Accept': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
window.location.href = '{{ route("admin.groups.index") }}';
} else {
alert(data.message || 'Error deleting group');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred');
});
}
}
</script>
@endpush
@endsection
Routes Configuration
// Add to routes/web.php
Route::prefix('admin')->name('admin.')->middleware(['auth'])->group(function () {
// Permission Groups Management
Route::prefix('groups')->name('groups.')->group(function () {
Route::get('/', [GroupController::class, 'index'])->name('index');
Route::get('/create', [GroupController::class, 'create'])->name('create');
Route::post('/', [GroupController::class, 'store'])->name('store');
Route::get('/{group}', [GroupController::class, 'show'])->name('show');
Route::get('/{group}/edit', [GroupController::class, 'edit'])->name('edit');
Route::put('/{group}', [GroupController::class, 'update'])->name('update');
Route::delete('/{group}', [GroupController::class, 'destroy'])->name('destroy');
// Additional routes
Route::post('/reorder', [GroupController::class, 'reorder'])->name('reorder');
Route::post('/{group}/move-permissions', [GroupController::class, 'movePermissions'])->name('move-permissions');
});
});
// Add to routes/web.php
Route::prefix('admin')->name('admin.')->middleware(['auth'])->group(function () {
// Permission Groups Management
Route::prefix('groups')->name('groups.')->group(function () {
Route::get('/', [GroupController::class, 'index'])->name('index');
Route::get('/create', [GroupController::class, 'create'])->name('create');
Route::post('/', [GroupController::class, 'store'])->name('store');
Route::get('/{group}', [GroupController::class, 'show'])->name('show');
Route::get('/{group}/edit', [GroupController::class, 'edit'])->name('edit');
Route::put('/{group}', [GroupController::class, 'update'])->name('update');
Route::delete('/{group}', [GroupController::class, 'destroy'])->name('destroy');
// Additional routes
Route::post('/reorder', [GroupController::class, 'reorder'])->name('reorder');
Route::post('/{group}/move-permissions', [GroupController::class, 'movePermissions'])->name('move-permissions');
});
});
JavaScript Functions for Group Views
// Group-specific JavaScript functions
// Auto-generate slug from name
document.getElementById('name')?.addEventListener('input', function(e) {
const slug = document.getElementById('slug');
if (!slug.dataset.manual) {
slug.value = e.target.value
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
});
// Mark slug as manually edited
document.getElementById('slug')?.addEventListener('input', function() {
this.dataset.manual = true;
});
// Fill group form with template
function fillGroup(name, slug, description) {
document.getElementById('name').value = name;
document.getElementById('slug').value = slug;
document.getElementById('description').value = description;
}
// Delete group
function deleteGroup(id, name) {
if (!confirm(`Are you sure you want to delete the group "${name}"?`)) {
return;
}
fetch(`/admin/groups/${id}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.message || 'Error deleting group');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the group');
});
}
// Enable reordering
let originalOrder = [];
function enableReorder() {
document.querySelectorAll('.reorder-handle').forEach(el => el.classList.remove('d-none'));
document.getElementById('reorderBtn').classList.add('d-none');
document.getElementById('saveOrderBtn').classList.remove('d-none');
document.getElementById('cancelOrderBtn').classList.remove('d-none');
// Store original order
originalOrder = [];
document.querySelectorAll('#groupsTable tbody tr').forEach(row => {
originalOrder.push({
id: row.dataset.id,
order: row.dataset.order
});
});
// Enable drag and drop
enableDragAndDrop();
}
// Cancel reordering
function cancelReorder() {
document.querySelectorAll('.reorder-handle').forEach(el => el.classList.add('d-none'));
document.getElementById('reorderBtn').classList.remove('d-none');
document.getElementById('saveOrderBtn').classList.add('d-none');
document.getElementById('cancelOrderBtn').classList.add('d-none');
// Restore original order if needed
location.reload();
}
// Save new order
function saveOrder() {
const groups = [];
let sortOrder = 10;
document.querySelectorAll('#groupsTable tbody tr').forEach(row => {
if (row.dataset.id) {
groups.push({
id: parseInt(row.dataset.id),
sort_order: sortOrder
});
sortOrder += 10;
}
});
fetch('/admin/groups/reorder', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ groups: groups })
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error saving order');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred');
});
}
// Enable drag and drop for reordering
function enableDragAndDrop() {
const tbody = document.querySelector('#groupsTable tbody');
if (!tbody) return;
let draggedElement = null;
tbody.querySelectorAll('tr').forEach(row => {
row.draggable = true;
row.style.cursor = 'move';
row.addEventListener('dragstart', function(e) {
draggedElement = this;
this.style.opacity = '0.5';
});
row.addEventListener('dragend', function(e) {
this.style.opacity = '';
});
row.addEventListener('dragover', function(e) {
e.preventDefault();
this.style.backgroundColor = '#f0f0f0';
});
row.addEventListener('dragleave', function(e) {
this.style.backgroundColor = '';
});
row.addEventListener('drop', function(e) {
e.preventDefault();
this.style.backgroundColor = '';
if (this !== draggedElement) {
if (this.parentNode) {
this.parentNode.insertBefore(draggedElement, this.nextSibling);
}
}
});
});
}
✓ Complete CRUD Package! You now have full CRUD examples for Roles, Permissions, and Permission Groups - all ready to copy and use in your Laravel application with OMNI Access.
Pro Tip: Add
<meta name="csrf-token" content="{{ csrf_token() }}">to your layout's head section for AJAX requests to work properly.
Note: Remember to create a base layout at
resources/views/layouts/admin.blade.phpwith Bootstrap 5 CSS and JS included.
// Group-specific JavaScript functions
// Auto-generate slug from name
document.getElementById('name')?.addEventListener('input', function(e) {
const slug = document.getElementById('slug');
if (!slug.dataset.manual) {
slug.value = e.target.value
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
}
});
// Mark slug as manually edited
document.getElementById('slug')?.addEventListener('input', function() {
this.dataset.manual = true;
});
// Fill group form with template
function fillGroup(name, slug, description) {
document.getElementById('name').value = name;
document.getElementById('slug').value = slug;
document.getElementById('description').value = description;
}
// Delete group
function deleteGroup(id, name) {
if (!confirm(`Are you sure you want to delete the group "${name}"?`)) {
return;
}
fetch(`/admin/groups/${id}`, {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.message || 'Error deleting group');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred while deleting the group');
});
}
// Enable reordering
let originalOrder = [];
function enableReorder() {
document.querySelectorAll('.reorder-handle').forEach(el => el.classList.remove('d-none'));
document.getElementById('reorderBtn').classList.add('d-none');
document.getElementById('saveOrderBtn').classList.remove('d-none');
document.getElementById('cancelOrderBtn').classList.remove('d-none');
// Store original order
originalOrder = [];
document.querySelectorAll('#groupsTable tbody tr').forEach(row => {
originalOrder.push({
id: row.dataset.id,
order: row.dataset.order
});
});
// Enable drag and drop
enableDragAndDrop();
}
// Cancel reordering
function cancelReorder() {
document.querySelectorAll('.reorder-handle').forEach(el => el.classList.add('d-none'));
document.getElementById('reorderBtn').classList.remove('d-none');
document.getElementById('saveOrderBtn').classList.add('d-none');
document.getElementById('cancelOrderBtn').classList.add('d-none');
// Restore original order if needed
location.reload();
}
// Save new order
function saveOrder() {
const groups = [];
let sortOrder = 10;
document.querySelectorAll('#groupsTable tbody tr').forEach(row => {
if (row.dataset.id) {
groups.push({
id: parseInt(row.dataset.id),
sort_order: sortOrder
});
sortOrder += 10;
}
});
fetch('/admin/groups/reorder', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({ groups: groups })
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error saving order');
}
})
.catch(error => {
console.error('Error:', error);
alert('An error occurred');
});
}
// Enable drag and drop for reordering
function enableDragAndDrop() {
const tbody = document.querySelector('#groupsTable tbody');
if (!tbody) return;
let draggedElement = null;
tbody.querySelectorAll('tr').forEach(row => {
row.draggable = true;
row.style.cursor = 'move';
row.addEventListener('dragstart', function(e) {
draggedElement = this;
this.style.opacity = '0.5';
});
row.addEventListener('dragend', function(e) {
this.style.opacity = '';
});
row.addEventListener('dragover', function(e) {
e.preventDefault();
this.style.backgroundColor = '#f0f0f0';
});
row.addEventListener('dragleave', function(e) {
this.style.backgroundColor = '';
});
row.addEventListener('drop', function(e) {
e.preventDefault();
this.style.backgroundColor = '';
if (this !== draggedElement) {
if (this.parentNode) {
this.parentNode.insertBefore(draggedElement, this.nextSibling);
}
}
});
});
}
Community Discussions 0
Ask questions, share step solutions, or discuss software workflows with other professionals.
Join the Community Discussion
Got stuck on a step or have a question about this configuration? Sign in to join the discussion and receive direct advice from instructors and peers.
No Discussions Yet
Have a question or found a clever tip for this guide? Start the conversation using the comment box above!