Update admin views and controllers

This commit is contained in:
DariusIII
2026-07-10 22:35:32 +02:00
parent f6c18cdeac
commit b0c781421c
10 changed files with 374 additions and 241 deletions
@@ -7,6 +7,7 @@ namespace App\Http\Controllers\Admin;
use App\Enums\SignupError;
use App\Enums\UserRole;
use App\Http\Controllers\BasePageController;
use App\Http\Requests\Admin\AdminUserListRequest;
use App\Models\Invitation;
use App\Models\User;
use App\Models\UserDownload;
@@ -28,43 +29,23 @@ class AdminUserController extends BasePageController
/**
* @throws \Throwable
*/
public function index(Request $request): View
public function index(AdminUserListRequest $request): View
{
$this->setAdminPrefs();
$meta_title = $title = 'User List';
$roles = Role::pluck('name', 'id')->toArray();
$roles = Role::query()->orderBy('name')->pluck('name', 'id')->toArray();
$ordering = getUserBrowseOrdering();
$orderBy = $request->has('ob') && \in_array($request->input('ob'), $ordering, false) ? $request->input('ob') : '';
$page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1;
$offset = ($page - 1) * config('nntmux.items_per_page');
$orderBy = $request->orderBy();
$variables = $request->filters();
$variables = [
'username' => $request->has('username') ? $request->input('username') : '',
'email' => $request->has('email') ? $request->input('email') : '',
'host' => $request->has('host') ? $request->input('host') : '',
'role' => $request->has('role') ? $request->input('role') : '',
'verified' => in_array($request->input('verified'), ['0', '1'], true) ? $request->input('verified') : '',
'created_from' => $request->has('created_from') ? $request->input('created_from') : '',
'created_to' => $request->has('created_to') ? $request->input('created_to') : '',
];
$result = User::getRange(
$offset,
(int) config('nntmux.items_per_page'),
$results = User::adminListPaginator(
$variables,
$orderBy,
$variables['username'],
$variables['email'],
$variables['host'],
$variables['role'],
$variables['created_from'],
$variables['created_to'],
$variables['verified']
);
$results = $this->paginate($result, User::getCount($variables['role'], $variables['username'], $variables['host'], $variables['email'], $variables['created_from'], $variables['created_to'], $variables['verified']), config('nntmux.items_per_page'), $page, $request->url(), $request->query());
(int) config('nntmux.items_per_page'),
)->withQueryString();
// Build order by URLs
$orderByUrls = [];
@@ -392,8 +373,9 @@ class AdminUserController extends BasePageController
$user->delete();
Cache::forget(AdminDashboardSnapshotService::CACHE_KEY);
// Redirect with username to display in notification
return redirect()->to('admin/user-list?deleted=1&username='.urlencode($username));
return redirect()
->route('admin.user-list')
->with('success', 'User "'.$username.'" has been deleted successfully.');
}
if ($request->has('redir')) {
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Http\Requests\Admin;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
class AdminUserListRequest extends FormRequest
{
public function authorize(): bool
{
$user = $this->user();
return $user instanceof User && $user->hasRole('Admin');
}
/**
* @return array<string, list<string>>
*/
public function rules(): array
{
return [
'username' => ['nullable', 'string', 'max:255'],
'email' => ['nullable', 'string', 'max:255'],
'host' => ['nullable', 'string', 'max:255'],
'role' => ['nullable', 'integer', 'exists:roles,id'],
'verified' => ['nullable', 'in:0,1'],
'created_from' => ['nullable', 'date_format:Y-m-d'],
'created_to' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:created_from'],
'ob' => ['nullable', 'string', 'in:'.implode(',', getUserBrowseOrdering())],
'page' => ['nullable', 'integer', 'min:1'],
];
}
/**
* @return array{username: string, email: string, host: string, role: string, verified: string, created_from: string, created_to: string}
*/
public function filters(): array
{
return [
'username' => $this->scalarInput('username'),
'email' => $this->scalarInput('email'),
'host' => $this->scalarInput('host'),
'role' => $this->scalarInput('role'),
'verified' => $this->scalarInput('verified'),
'created_from' => $this->scalarInput('created_from'),
'created_to' => $this->scalarInput('created_to'),
];
}
public function orderBy(): string
{
$orderBy = $this->scalarInput('ob');
return \in_array($orderBy, getUserBrowseOrdering(), true) ? $orderBy : '';
}
private function scalarInput(string $key): string
{
$value = $this->input($key, '');
return is_scalar($value) ? trim((string) $value) : '';
}
}
+2
View File
@@ -506,6 +506,8 @@ class Release extends Model
'video:id,title,tvdb,trakt,tvrage,tvmaze,source',
'video.tvInfo:videos_id,summary,image',
'episode:id,title,firstaired,se_complete',
'releaseGroup:releases_id,groups_id',
'releaseGroup.group:id,name',
]);
if (is_array($guid)) {
+64 -24
View File
@@ -15,6 +15,7 @@ use Carbon\CarbonImmutable;
use Illuminate\Auth\MustVerifyEmail as MustVerifyEmailTrait;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Contracts\Auth\MustVerifyEmail as MustVerifyEmailContract;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
@@ -755,20 +756,15 @@ final class User extends Authenticatable implements CanResetPasswordContract, Ha
?string $createdTo = '',
?string $verified = '',
): int {
return self::query()
->withTrashed()
->excludeSharing()
->when($role, fn (Builder $q) => $q->where('roles_id', $role))
->when($username, fn (Builder $q) => $q->where('username', 'like', "%{$username}%"))
->when($host, fn (Builder $q) => $q->where('host', 'like', "%{$host}%"))
->when($email, fn (Builder $q) => $q->where('email', 'like', "%{$email}%"))
->when($createdFrom, fn (Builder $q) => $q->where('created_at', '>=', "{$createdFrom} 00:00:00"))
->when($createdTo, fn (Builder $q) => $q->where('created_at', '<=', "{$createdTo} 23:59:59"))
->when($verified === '1', fn (Builder $q) => $q->where(function (Builder $verifiedQuery): void {
$verifiedQuery->where('verified', true)
->orWhereNotNull('email_verified_at');
}))
->when($verified === '0', fn (Builder $q) => $q->where('verified', false)->whereNull('email_verified_at'))
return self::adminListBaseQuery([
'role' => (string) $role,
'username' => (string) $username,
'host' => (string) $host,
'email' => (string) $email,
'created_from' => (string) $createdFrom,
'created_to' => (string) $createdTo,
'verified' => (string) $verified,
])
->count();
}
@@ -1355,6 +1351,57 @@ final class User extends Authenticatable implements CanResetPasswordContract, Ha
): Collection {
$order = self::getBrowseOrder($orderBy);
return self::adminListBaseQuery([
'username' => (string) $userName,
'email' => (string) $email,
'host' => (string) $host,
'role' => (string) $role,
'created_from' => (string) $createdFrom,
'created_to' => (string) $createdTo,
'verified' => (string) $verified,
])
->withCount([
'requests as daily_api_count' => fn (Builder $q) => $q->where('timestamp', '>', now()->subDay()),
'downloads as daily_download_count' => fn (Builder $q) => $q->where('timestamp', '>', now()->subDay()),
])
->with('roles:id,name')
->orderBy($order[0], $order[1])
->when($start !== false, fn (Builder $q) => $q->offset($start)->limit($offset))
->get();
}
/**
* @param array{username?: string, email?: string, host?: string, role?: string, verified?: string, created_from?: string, created_to?: string} $filters
* @return LengthAwarePaginator<int, User>
*/
public static function adminListPaginator(array $filters, string $orderBy, int $perPage): LengthAwarePaginator
{
$order = self::getBrowseOrder($orderBy);
return self::adminListBaseQuery($filters)
->withCount([
'requests as daily_api_count' => fn (Builder $q) => $q->where('timestamp', '>', now()->subDay()),
'downloads as daily_download_count' => fn (Builder $q) => $q->where('timestamp', '>', now()->subDay()),
])
->with('roles:id,name')
->orderBy($order[0], $order[1])
->paginate($perPage);
}
/**
* @param array{username?: string, email?: string, host?: string, role?: string, verified?: string, created_from?: string, created_to?: string} $filters
* @return Builder<User>
*/
private static function adminListBaseQuery(array $filters): Builder
{
$username = $filters['username'] ?? '';
$email = $filters['email'] ?? '';
$host = $filters['host'] ?? '';
$role = $filters['role'] ?? '';
$verified = $filters['verified'] ?? '';
$createdFrom = $filters['created_from'] ?? '';
$createdTo = $filters['created_to'] ?? '';
return self::query()
->withTrashed()
->select('users.*')
@@ -1363,7 +1410,7 @@ final class User extends Authenticatable implements CanResetPasswordContract, Ha
'rolename'
)
->excludeSharing()
->when($userName, fn (Builder $q) => $q->where('username', 'like', "%{$userName}%"))
->when($username, fn (Builder $q) => $q->where('username', 'like', "%{$username}%"))
->when($email, fn (Builder $q) => $q->where('email', 'like', "%{$email}%"))
->when($host, fn (Builder $q) => $q->where('host', 'like', "%{$host}%"))
->when($role, fn (Builder $q) => $q->where('roles_id', $role))
@@ -1373,15 +1420,7 @@ final class User extends Authenticatable implements CanResetPasswordContract, Ha
$verifiedQuery->where('verified', true)
->orWhereNotNull('email_verified_at');
}))
->when($verified === '0', fn (Builder $q) => $q->where('verified', false)->whereNull('email_verified_at'))
->withCount([
'requests as daily_api_count' => fn (Builder $q) => $q->where('timestamp', '>', now()->subDay()),
'downloads as daily_download_count' => fn (Builder $q) => $q->where('timestamp', '>', now()->subDay()),
])
->with('roles:id,name')
->orderBy($order[0], $order[1])
->when($start !== false, fn (Builder $q) => $q->offset($start)->limit($offset))
->get();
->when($verified === '0', fn (Builder $q) => $q->where('verified', false)->whereNull('email_verified_at'));
}
/**
@@ -1400,6 +1439,7 @@ final class User extends Authenticatable implements CanResetPasswordContract, Ha
'createdat' => 'created_at',
'lastlogin' => 'lastlogin',
'apiaccess' => 'apiaccess',
'apirequests' => 'daily_api_count',
'grabs' => 'grabs',
'role' => 'rolename',
'rolechangedate' => 'rolechangedate',
+10 -6
View File
@@ -29,6 +29,7 @@ use App\Observers\ReleaseObserver;
use App\Observers\RolePromotionObserver;
use App\Observers\SteamAppObserver;
use App\Observers\VideoObserver;
use App\View\Composers\AdminDataComposer;
use App\View\Composers\GlobalDataComposer;
use Illuminate\Auth\Events\Login;
use Illuminate\Pagination\Paginator;
@@ -46,19 +47,22 @@ class AppServiceProvider extends ServiceProvider
{
Paginator::useTailwind();
// Share global data with layouts and all admin views
// Admin child views need direct registration because @section blocks
// are evaluated before the layout composer runs
// Same for search.* so category menu data (e.g. parentcatlist) exists in @section('content')
// Share public-site global data with layouts that need menu/category data.
// search.* needs direct registration because @section blocks are evaluated
// before the layout composer runs.
view()->composer([
'layouts.main',
'layouts.admin',
'layouts.guest',
'layouts.app',
'admin.*',
'search.*',
], GlobalDataComposer::class);
// Admin pages use a leaner composer and do not need public-site menu data.
view()->composer([
'layouts.admin',
'admin.*',
], AdminDataComposer::class);
Gate::define('viewPulse', function (User $user) {
return $user->hasRole('Admin');
});
+66
View File
@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\View\Composers;
use App\Models\Settings;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Illuminate\View\View;
class AdminDataComposer
{
/**
* Cache TTL in seconds (5 minutes).
*/
private const CACHE_TTL = 300;
/**
* Bind lightweight admin data to the view.
*/
public function compose(View $view): void
{
$user = Auth::user();
$isNntmuxUser = $user instanceof User;
$view->with([
'serverroot' => url('/'),
'site' => $this->rememberWithCacheFallback('site_settings_array', self::CACHE_TTL, function () {
return Settings::query()
->pluck('value', 'name')
->map(fn ($value) => Settings::convertValue($value))
->all();
}),
'userdata' => $user,
'loggedin' => $user !== null,
'isadmin' => $isNntmuxUser && $user->hasRole('Admin'),
'ismod' => $isNntmuxUser && $user->hasRole('Moderator'),
]);
}
/**
* @param callable(): mixed $callback
*/
private function rememberWithCacheFallback(string $key, int|\DateInterval $ttl, callable $callback): mixed
{
try {
if (Cache::has($key)) {
return Cache::get($key);
}
$value = $callback();
Cache::put($key, $value, $ttl);
return $value;
} catch (\Throwable $e) {
if (config('app.debug')) {
Log::debug('AdminDataComposer cache bypassed: '.$e->getMessage());
}
return $callback();
}
}
}
@@ -216,149 +216,6 @@ Alpine.data('verifyUser', () => ({
}
}));
// User List Scroll Sync
Alpine.data('adminUserList', () => ({
allChecked: false,
init() {
const top = document.getElementById('topScroll');
const bottom = document.getElementById('bottomScroll');
const content = document.getElementById('topScrollContent');
const table = bottom?.querySelector('table');
if (top && bottom && content && table) {
const sync = () => { content.style.width = table.scrollWidth + 'px'; };
sync();
window.addEventListener('resize', sync);
top.addEventListener('scroll', function() { if (!top._syncing) { bottom._syncing = true; bottom.scrollLeft = top.scrollLeft; bottom._syncing = false; } });
bottom.addEventListener('scroll', function() { if (!bottom._syncing) { top._syncing = true; top.scrollLeft = bottom.scrollLeft; bottom._syncing = false; } });
}
const selectAll = this.$el.querySelector('#selectAllUserList');
const form = this.$el.querySelector('#bulkUserActionForm');
if (selectAll) {
selectAll.addEventListener('change', () => this.toggleAll());
}
this.$el.querySelectorAll('.user-list-checkbox').forEach(cb => {
cb.addEventListener('change', () => this.onCheckboxChange());
});
if (form) {
form.addEventListener('submit', e => this.submitBulkAction(e));
}
},
_checkboxes() {
return Array.from(this.$el.querySelectorAll('.user-list-checkbox:not(:disabled)'));
},
_checkedBoxes() {
return this._checkboxes().filter(cb => cb.checked);
},
_checkedUnverifiedBoxes() {
return this._checkedBoxes().filter(cb => cb.dataset.isVerified === '0');
},
_setValidationError(message) {
const errEl = document.getElementById('validationError');
const errMsg = document.getElementById('validationErrorMessage');
if (errEl && errMsg) {
errMsg.textContent = message;
errEl.classList.remove('hidden');
}
},
_clearValidationError() {
const errEl = document.getElementById('validationError');
if (errEl) errEl.classList.add('hidden');
},
toggleAll() {
const selectAll = this.$el.querySelector('#selectAllUserList');
this._checkboxes().forEach(cb => { cb.checked = selectAll?.checked ?? false; });
this.onCheckboxChange();
},
onCheckboxChange() {
const selectAll = this.$el.querySelector('#selectAllUserList');
const boxes = this._checkboxes();
const checked = this._checkedBoxes();
this.allChecked = boxes.length > 0 && checked.length === boxes.length;
if (selectAll) {
selectAll.checked = this.allChecked;
selectAll.indeterminate = checked.length > 0 && checked.length < boxes.length;
}
},
submitBulkAction(e) {
e.preventDefault();
this._clearValidationError();
const form = this.$el.querySelector('#bulkUserActionForm');
const action = this.$el.querySelector('#bulkUserAction')?.value;
const checked = this._checkedBoxes();
const checkedCount = checked.length;
if (!action) { this._setValidationError('Please select an action.'); return; }
if (checkedCount === 0) { this._setValidationError('Please select at least one non-admin active user.'); return; }
const actions = {
delete: {
title: 'Soft Delete Users',
actionText: 'soft-delete',
type: 'danger',
confirmText: 'Soft Delete'
},
verify: {
title: 'Verify Users',
actionText: 'mark as verified',
type: 'success',
confirmText: 'Verify'
},
resend_verification: {
title: 'Resend Verification Emails',
actionText: 'send account verification emails to',
type: 'warning',
confirmText: 'Send Emails'
}
};
const config = actions[action];
if (!config) { this._setValidationError('Please select a valid action.'); return; }
let targetCount = checkedCount;
let message = 'Are you sure you want to ' + config.actionText + ' ' + targetCount + ' user(s)?';
if (action === 'resend_verification') {
const unverifiedCount = this._checkedUnverifiedBoxes().length;
const skippedCount = checkedCount - unverifiedCount;
if (unverifiedCount === 0) {
this._setValidationError('Please select at least one not verified user to resend verification email.');
return;
}
targetCount = unverifiedCount;
message = 'Are you sure you want to send account verification emails to ' + targetCount + ' not verified user(s)?';
if (skippedCount > 0) {
message += ' ' + skippedCount + ' already verified user(s) will be skipped.';
}
}
showConfirm({
title: config.title,
message: message,
type: config.type,
confirmText: config.confirmText,
onConfirm: function() { if (form) form.submit(); }
});
}
}));
// Admin deleted users
Alpine.data('adminDeletedUsers', () => ({
allChecked: false,
@@ -0,0 +1,146 @@
/**
* Alpine.data('adminUserList') - Admin user list bulk selection and scroll sync.
*/
import Alpine from '@alpinejs/csp';
Alpine.data('adminUserList', () => ({
allChecked: false,
init() {
const top = document.getElementById('topScroll');
const bottom = document.getElementById('bottomScroll');
const content = document.getElementById('topScrollContent');
const table = bottom?.querySelector('table');
if (top && bottom && content && table) {
const sync = () => { content.style.width = table.scrollWidth + 'px'; };
sync();
window.addEventListener('resize', sync);
top.addEventListener('scroll', function() { if (!top._syncing) { bottom._syncing = true; bottom.scrollLeft = top.scrollLeft; bottom._syncing = false; } });
bottom.addEventListener('scroll', function() { if (!bottom._syncing) { top._syncing = true; top.scrollLeft = bottom.scrollLeft; bottom._syncing = false; } });
}
const selectAll = this.$el.querySelector('#selectAllUserList');
const form = this.$el.querySelector('#bulkUserActionForm');
if (selectAll) {
selectAll.addEventListener('change', () => this.toggleAll());
}
this.$el.querySelectorAll('.user-list-checkbox').forEach(cb => {
cb.addEventListener('change', () => this.onCheckboxChange());
});
if (form) {
form.addEventListener('submit', e => this.submitBulkAction(e));
}
},
_checkboxes() {
return Array.from(this.$el.querySelectorAll('.user-list-checkbox:not(:disabled)'));
},
_checkedBoxes() {
return this._checkboxes().filter(cb => cb.checked);
},
_checkedUnverifiedBoxes() {
return this._checkedBoxes().filter(cb => cb.dataset.isVerified === '0');
},
_setValidationError(message) {
const errEl = document.getElementById('validationError');
const errMsg = document.getElementById('validationErrorMessage');
if (errEl && errMsg) {
errMsg.textContent = message;
errEl.classList.remove('hidden');
}
},
_clearValidationError() {
const errEl = document.getElementById('validationError');
if (errEl) errEl.classList.add('hidden');
},
toggleAll() {
const selectAll = this.$el.querySelector('#selectAllUserList');
this._checkboxes().forEach(cb => { cb.checked = selectAll?.checked ?? false; });
this.onCheckboxChange();
},
onCheckboxChange() {
const selectAll = this.$el.querySelector('#selectAllUserList');
const boxes = this._checkboxes();
const checked = this._checkedBoxes();
this.allChecked = boxes.length > 0 && checked.length === boxes.length;
if (selectAll) {
selectAll.checked = this.allChecked;
selectAll.indeterminate = checked.length > 0 && checked.length < boxes.length;
}
},
submitBulkAction(e) {
e.preventDefault();
this._clearValidationError();
const form = this.$el.querySelector('#bulkUserActionForm');
const action = this.$el.querySelector('#bulkUserAction')?.value;
const checked = this._checkedBoxes();
const checkedCount = checked.length;
if (!action) { this._setValidationError('Please select an action.'); return; }
if (checkedCount === 0) { this._setValidationError('Please select at least one non-admin active user.'); return; }
const actions = {
delete: {
title: 'Soft Delete Users',
actionText: 'soft-delete',
type: 'danger',
confirmText: 'Soft Delete',
},
verify: {
title: 'Verify Users',
actionText: 'mark as verified',
type: 'success',
confirmText: 'Verify',
},
resend_verification: {
title: 'Resend Verification Emails',
actionText: 'send account verification emails to',
type: 'warning',
confirmText: 'Send Emails',
},
};
const config = actions[action];
if (!config) { this._setValidationError('Please select a valid action.'); return; }
let targetCount = checkedCount;
let message = 'Are you sure you want to ' + config.actionText + ' ' + targetCount + ' user(s)?';
if (action === 'resend_verification') {
const unverifiedCount = this._checkedUnverifiedBoxes().length;
const skippedCount = checkedCount - unverifiedCount;
if (unverifiedCount === 0) {
this._setValidationError('Please select at least one not verified user to resend verification email.');
return;
}
targetCount = unverifiedCount;
message = 'Are you sure you want to send account verification emails to ' + targetCount + ' not verified user(s)?';
if (skippedCount > 0) {
message += ' ' + skippedCount + ' already verified user(s) will be skipped.';
}
}
showConfirm({
title: config.title,
message: message,
type: config.type,
confirmText: config.confirmText,
onConfirm: function() { if (form) form.submit(); },
});
},
}));
+1 -1
View File
@@ -51,7 +51,7 @@ const lazyComponentMap = {
'adminGroups': () => import('./components/admin/groups.js'),
'adminFeatures': () => import('./components/admin/features.js'),
'adminUserEdit': () => import('./components/admin/features.js'), // same file
'adminUserList': () => import('./components/admin/features.js'), // same file
'adminUserList': () => import('./components/admin/user-list.js'),
'adminDeletedUsers': () => import('./components/admin/features.js'), // same file
'adminInvitations': () => import('./components/admin/features.js'), // same file
'adminRegexForm': () => import('./components/admin/features.js'), // same file
+7 -37
View File
@@ -3,20 +3,16 @@
@section('content')
<div class="space-y-6" x-data="adminUserList">
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
<!-- Header -->
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-between items-center">
<h1 class="text-2xl font-semibold text-gray-800 dark:text-gray-200">
<i class="fas fa-users mr-2"></i>{{ $title }}
</h1>
<x-admin.page-header :title="$title" icon="fas fa-users">
<x-slot:actions>
<a href="{{ url('admin/user-edit?action=add') }}" class="px-4 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800">
<i class="fas fa-plus mr-2"></i>Add New User
</a>
</div>
</div>
</x-slot:actions>
</x-admin.page-header>
<!-- Search Filters -->
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<x-admin.search-bar>
<form method="get" action="{{ url('admin/user-list') }}">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div>
@@ -95,34 +91,9 @@
</a>
</div>
</form>
</div>
<!-- Success/Error Messages -->
@if(request()->has('deleted') && request()->input('deleted') == 1)
<div class="mx-6 mt-4 p-4 bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg">
<p class="text-green-800 dark:text-green-200">
<i class="fas fa-check-circle mr-2"></i>
User "{{ request()->input('username') }}" has been deleted successfully.
</p>
</div>
@endif
@if(session('success'))
<div class="mx-6 mt-4 p-4 bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-lg">
<p class="text-green-800 dark:text-green-200">
<i class="fas fa-check-circle mr-2"></i>{{ session('success') }}
</p>
</div>
@endif
@if(session('error'))
<div class="mx-6 mt-4 p-4 bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg">
<p class="text-red-800 dark:text-red-200">
<i class="fas fa-exclamation-circle mr-2"></i>{{ session('error') }}
</p>
</div>
@endif
</x-admin.search-bar>
<!-- Validation Messages -->
@if($errors->any())
<div class="mx-6 mt-4 p-4 bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 rounded-lg">
<p class="text-red-800 dark:text-red-200">
@@ -489,4 +460,3 @@
@endsection
{{-- Scripts moved to resources/js/csp-safe.js --}}