428 lines
18 KiB
PHP
428 lines
18 KiB
PHP
@extends('layouts.master_two')
|
|
|
|
@section('content')
|
|
<div class="container py-4">
|
|
<div class="d-flex align-items-center mb-3">
|
|
<h3 class="me-auto">Manage Users</h3>
|
|
<div class="text-muted">June 15, 2026</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<ul class="nav nav-tabs mb-3" id="userTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="district-tab" data-bs-toggle="tab" data-bs-target="#district" type="button" role="tab" aria-controls="district" aria-selected="true">District <span class="badge bg-secondary ms-1" id="count-district">0</span></button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="regional-tab" data-bs-toggle="tab" data-bs-target="#regional" type="button" role="tab" aria-controls="regional" aria-selected="false">Regional <span class="badge bg-secondary ms-1" id="count-regional">0</span></button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="national-tab" data-bs-toggle="tab" data-bs-target="#national" type="button" role="tab" aria-controls="national" aria-selected="false">National <span class="badge bg-secondary ms-1" id="count-national">0</span></button>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Shared toolbar -->
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<div class="toolbar">
|
|
<div class="d-flex align-items-center">
|
|
<input type="checkbox" id="globalSelect" class="form-check-input select-all" aria-label="Select all visible users">
|
|
<div class="me-2">Select</div>
|
|
</div>
|
|
|
|
<div class="input-group" style="max-width:420px;">
|
|
<span class="input-group-text">Search</span>
|
|
<input id="globalSearch" class="form-control" type="search" placeholder="Search name, email or ID (press /)" aria-label="Search users">
|
|
</div>
|
|
|
|
<div class="ms-auto d-flex gap-2">
|
|
<div class="dropdown">
|
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Filter</button>
|
|
<ul class="dropdown-menu p-3" style="min-width:220px;">
|
|
<div class="mb-2"><strong>Status</strong></div>
|
|
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Active" id="fActive"><label class="form-check-label" for="fActive">Active</label></div>
|
|
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Pending" id="fPending"><label class="form-check-label" for="fPending">Pending</label></div>
|
|
<div class="form-check"><input class="form-check-input filter-status" type="checkbox" value="Suspended" id="fSuspended"><label class="form-check-label" for="fSuspended">Suspended</label></div>
|
|
</ul>
|
|
</div>
|
|
|
|
<button id="bulkMoveBtn" class="btn btn-primary" disabled data-bs-toggle="modal" data-bs-target="#moveModal">Move</button>
|
|
<button id="bulkDeactivate" class="btn btn-outline-danger" disabled>Deactivate</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tab content -->
|
|
<div class="tab-content" id="userTabsContent">
|
|
<!-- District -->
|
|
<div class="tab-pane fade show active" id="district" role="tabpanel" aria-labelledby="district-tab">
|
|
<div class="card mb-4">
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width:42px;"></th>
|
|
<th>Person</th>
|
|
<th>Email</th>
|
|
<th>Role</th>
|
|
<th>Last activity</th>
|
|
<th style="width:130px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="districtBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Regional -->
|
|
<div class="tab-pane fade" id="regional" role="tabpanel" aria-labelledby="regional-tab">
|
|
<div class="card mb-4">
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width:42px;"></th>
|
|
<th>Person</th>
|
|
<th>Email</th>
|
|
<th>Role</th>
|
|
<th>Last activity</th>
|
|
<th style="width:130px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="regionalBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- National -->
|
|
<div class="tab-pane fade" id="national" role="tabpanel" aria-labelledby="national-tab">
|
|
<div class="card mb-4">
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th style="width:42px;"></th>
|
|
<th>Person</th>
|
|
<th>Email</th>
|
|
<th>Role</th>
|
|
<th>Last activity</th>
|
|
<th style="width:130px;">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="nationalBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination stub -->
|
|
<nav aria-label="User list pages" class="d-flex justify-content-end">
|
|
<ul class="pagination pagination-sm" id="paginationContainer">
|
|
<li class="page-item disabled"><a class="page-link" href="#" tabindex="-1">Prev</a></li>
|
|
<li class="page-item active"><a class="page-link" href="#">1</a></li>
|
|
<li class="page-item"><a class="page-link" href="#">2</a></li>
|
|
<li class="page-item"><a class="page-link" href="#">Next</a></li>
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
|
|
<!-- Edit modal -->
|
|
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-sm modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="editModalLabel">Edit user</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="editForm">
|
|
<div class="modal-body">
|
|
<input type="hidden" id="editId">
|
|
<div class="mb-3"><label class="form-label">Name</label><input id="editName" class="form-control"></div>
|
|
<div class="mb-3"><label class="form-label">Email</label><input id="editEmail" class="form-control" type="email"></div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Category</label>
|
|
<select id="editCategory" class="form-select">
|
|
<option>District</option>
|
|
<option>Regional</option>
|
|
<option>National</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Save</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Move modal (bulk) -->
|
|
<div class="modal fade" id="moveModal" tabindex="-1" aria-labelledby="moveModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-sm modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="moveModalLabel">Move selected users</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="moveForm">
|
|
<div class="modal-body">
|
|
<p id="moveCountText">Move <span id="moveCount">0</span> users to:</p>
|
|
<div class="mb-3">
|
|
<select id="moveTarget" class="form-select" required>
|
|
<option value="">Choose category</option>
|
|
<option value="District">District</option>
|
|
<option value="Regional">Regional</option>
|
|
<option value="National">National</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Move</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@push('styles')
|
|
<style>
|
|
/* Small visual tweaks */
|
|
.category-badge { font-weight: 600; color: #fff; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: .8rem; }
|
|
.badge-district { background:#f6a623; } /* amber */
|
|
.badge-regional { background:#17a2b8; } /* teal */
|
|
.badge-national { background:#1f6feb; } /* blue */
|
|
.user-row:hover .row-actions { visibility: visible; opacity: 1; }
|
|
.row-actions { visibility: hidden; opacity: 0; transition: .12s ease-in-out; }
|
|
.table-avatar { width:36px; height:36px; object-fit:cover; border-radius:50%; }
|
|
.toolbar { gap:.5rem; display:flex; flex-wrap:wrap; align-items:center; }
|
|
.select-all { margin-right:.5rem; }
|
|
@media (max-width: 767px) {
|
|
.toolbar { flex-direction:column; align-items:stretch; }
|
|
}
|
|
</style>
|
|
@endpush
|
|
|
|
@push('scripts')
|
|
<!-- jQuery + Bootstrap bundle -->
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
|
|
|
<script>
|
|
const routes = {
|
|
list: "{{ route('admin.users.list') }}",
|
|
show: (id)=> "{{ url('admin/users') }}/"+id,
|
|
update: (id)=> "{{ url('admin/users') }}/"+id,
|
|
move: "{{ route('admin.users.move') }}",
|
|
deactivate: "{{ route('admin.users.deactivate') }}"
|
|
};
|
|
|
|
$.ajaxSetup({ headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' } });
|
|
|
|
(function(){
|
|
// keyboard shortcut to focus search
|
|
$(document).on('keydown', function(e){
|
|
if (e.key === '/' && !$(e.target).is('input, textarea')) {
|
|
e.preventDefault();
|
|
$('#globalSearch').focus().select();
|
|
}
|
|
});
|
|
|
|
let currentCategory = 'District';
|
|
|
|
function renderRow(u){
|
|
const badgeClass = u.category === 'District' ? 'badge-district' : (u.category === 'Regional' ? 'badge-regional' : 'badge-national');
|
|
const avatar = u.avatar_url || 'https://i.pravatar.cc/40';
|
|
const role = u.role || '';
|
|
const lastAct = u.last_activity_at || '';
|
|
const row = $(`
|
|
<tr class="user-row" data-category="${u.category}" data-status="${u.status}" data-name="${u.name}" data-email="${u.email}" data-id="${u.id}">
|
|
<td><input class="row-select form-check-input" type="checkbox" aria-label="Select ${u.name}"></td>
|
|
<td class="align-middle">
|
|
<div class="d-flex align-items-center">
|
|
<img src="${avatar}" alt="avatar" class="table-avatar me-2">
|
|
<div><div class="fw-semibold">${u.name}</div><div class="text-muted small">ID: ${u.id}</div></div>
|
|
</div>
|
|
</td>
|
|
<td class="align-middle">${u.email}</td>
|
|
<td class="align-middle"><span class="category-badge ${badgeClass}">${u.category}</span> · ${role}</td>
|
|
<td class="align-middle">${lastAct}</td>
|
|
<td class="align-middle">
|
|
<div class="row-actions d-flex gap-2">
|
|
<button class="btn btn-sm btn-outline-secondary viewBtn" data-id="${u.id}">View</button>
|
|
<button class="btn btn-sm btn-primary editBtn" data-id="${u.id}" data-bs-toggle="modal" data-bs-target="#editModal">Edit</button>
|
|
</div>
|
|
</td>
|
|
</tr>`);
|
|
const body = u.category === 'District' ? '#districtBody' : (u.category === 'Regional' ? '#regionalBody' : '#nationalBody');
|
|
$(body).append(row);
|
|
}
|
|
|
|
function clearBodies(){ $('#districtBody,#regionalBody,#nationalBody').empty(); }
|
|
|
|
function fetchCategory(category){
|
|
currentCategory = category;
|
|
const params = { category, q: $('#globalSearch').val(), status: $('.filter-status:checked').map(function(){return this.value}).get() };
|
|
$.get(routes.list, params, function(data){
|
|
clearBodies();
|
|
const items = data.data || data;
|
|
items.forEach(renderRow);
|
|
// Try to use paginator meta for counts, fall back to counting rendered rows
|
|
$('#count-district').text(data.meta?.total_by_category?.District ?? $('#districtBody .user-row').length);
|
|
$('#count-regional').text(data.meta?.total_by_category?.Regional ?? $('#regionalBody .user-row').length);
|
|
$('#count-national').text(data.meta?.total_by_category?.National ?? $('#nationalBody .user-row').length);
|
|
});
|
|
}
|
|
|
|
// Initial load per category
|
|
['District','Regional','National'].forEach(c => {
|
|
$.get(routes.list, { category: c }, function(data){
|
|
(data.data || data).forEach(renderRow);
|
|
$('#count-'+c.toLowerCase()).text(data.meta?.total ?? (data.data || data).length);
|
|
});
|
|
});
|
|
|
|
// Select all visible in active tab
|
|
function getVisibleRowsInActiveTab(){
|
|
const activePane = $('.tab-pane.active');
|
|
return activePane.find('.user-row:visible');
|
|
}
|
|
|
|
$('#globalSelect').on('change', function(){
|
|
const checked = $(this).is(':checked');
|
|
getVisibleRowsInActiveTab().find('.row-select').prop('checked', checked).trigger('change');
|
|
});
|
|
|
|
// Row checkbox toggles bulk buttons
|
|
$(document).on('change', '.row-select', function(){
|
|
const anyChecked = $('.tab-pane.active .row-select:checked').length > 0;
|
|
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', !anyChecked);
|
|
const total = getVisibleRowsInActiveTab().find('.row-select').length;
|
|
const checked = getVisibleRowsInActiveTab().find('.row-select:checked').length;
|
|
$('#globalSelect').prop('checked', total>0 && total===checked);
|
|
});
|
|
|
|
// Search + filter debounce
|
|
let t=null;
|
|
$('#globalSearch, .filter-status').on('input change', function(){
|
|
clearTimeout(t);
|
|
t = setTimeout(()=> { fetchCategory(currentCategory); }, 300);
|
|
});
|
|
|
|
// Activate tab event -> set currentCategory and reset selects
|
|
$('button[data-bs-toggle="tab"]').on('shown.bs.tab', function(e){
|
|
const target = $(e.target).data('bsTarget') || $(e.target).data('bs-target');
|
|
currentCategory = target.replace('#','');
|
|
$('#globalSelect').prop('checked', false);
|
|
$('#bulkMoveBtn, #bulkDeactivate').prop('disabled', true);
|
|
});
|
|
|
|
// Edit: fetch details and populate modal
|
|
let lastTrigger = null;
|
|
$(document).on('click', '.editBtn', function(){
|
|
lastTrigger = this;
|
|
const id = $(this).data('id');
|
|
$.get(routes.show(id), function(user){
|
|
$('#editId').val(user.id);
|
|
$('#editName').val(user.name);
|
|
$('#editEmail').val(user.email);
|
|
$('#editCategory').val(user.category);
|
|
});
|
|
});
|
|
|
|
// When edit modal closes, return focus to trigger
|
|
$('#editModal').on('hidden.bs.modal', function(){
|
|
if (lastTrigger && document.contains(lastTrigger)) lastTrigger.focus();
|
|
lastTrigger = null;
|
|
});
|
|
|
|
// Update via AJAX
|
|
$('#editForm').on('submit', function(e){
|
|
e.preventDefault();
|
|
const id = $('#editId').val();
|
|
const payload = { name: $('#editName').val(), email: $('#editEmail').val(), category: $('#editCategory').val() };
|
|
$.ajax({
|
|
url: routes.update(id),
|
|
method: 'PUT',
|
|
data: payload,
|
|
success: function(updated){
|
|
const row = $(`.user-row[data-id="${updated.id}"]`);
|
|
if (row.length){
|
|
row.data('name', updated.name).data('email', updated.email).data('category', updated.category);
|
|
row.find('.fw-semibold').text(updated.name);
|
|
row.find('td').eq(2).text(updated.email);
|
|
row.find('.category-badge').removeClass('badge-district badge-regional badge-national');
|
|
if (updated.category === 'District') row.find('.category-badge').addClass('badge-district').text('District');
|
|
if (updated.category === 'Regional') row.find('.category-badge').addClass('badge-regional').text('Regional');
|
|
if (updated.category === 'National') row.find('.category-badge').addClass('badge-national').text('National');
|
|
const target = updated.category === 'District' ? '#districtBody' : (updated.category === 'Regional' ? '#regionalBody' : '#nationalBody');
|
|
if (!row.parent().is(target)) row.appendTo($(target));
|
|
} else {
|
|
fetchCategory(currentCategory);
|
|
}
|
|
$('#editModal').modal('hide');
|
|
},
|
|
error: function(xhr){
|
|
if (xhr.status === 422) {
|
|
alert('Validation error');
|
|
} else alert('Update failed');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Bulk move: prepare modal count
|
|
$('#bulkMoveBtn').on('click', function(){
|
|
const selected = $('.tab-pane.active .row-select:checked');
|
|
$('#moveCount').text(selected.length);
|
|
$('#moveTarget').val('');
|
|
$('#moveModal').on('shown.bs.modal', function(){ $('#moveTarget').focus(); });
|
|
});
|
|
|
|
// Execute move
|
|
$('#moveForm').on('submit', function(e){
|
|
e.preventDefault();
|
|
const ids = $('.tab-pane.active .row-select:checked').closest('.user-row').map(function(){ return $(this).data('id'); }).get();
|
|
const target = $('#moveTarget').val();
|
|
$.post(routes.move, { ids, target }, function(res){
|
|
fetchCategory(currentCategory);
|
|
$('#moveModal').modal('hide');
|
|
}).fail(function(){ alert('Move failed'); });
|
|
});
|
|
|
|
// Bulk deactivate
|
|
$('#bulkDeactivate').on('click', function(){
|
|
const ids = $('.tab-pane.active .row-select:checked').closest('.user-row').map(function(){ return $(this).data('id'); }).get();
|
|
if (!ids.length) return;
|
|
$.post(routes.deactivate, { ids }, function(res){
|
|
fetchCategory(currentCategory);
|
|
}).fail(function(){ alert('Deactivate failed'); });
|
|
});
|
|
|
|
// init counts if none loaded
|
|
setTimeout(()=> {
|
|
$('#count-district').text($('#districtBody .user-row').length);
|
|
$('#count-regional').text($('#regionalBody .user-row').length);
|
|
$('#count-national').text($('#nationalBody .user-row').length);
|
|
}, 400);
|
|
|
|
// accessibility shortcut example
|
|
$(document).on('keydown', function(e){
|
|
if (e.key === 'e' && !$(e.target).is('input, textarea')) {
|
|
e.preventDefault();
|
|
$('.editBtn').first().focus();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
@endpush
|