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