mirror of
https://github.com/LD-Reborn/Berufsschule_HAM.git
synced 2025-12-20 06:51:55 +00:00
Add server-side filtering with AJAX pagination for users
This commit is contained in:
@@ -75,7 +75,14 @@ public class HomeController : Controller
|
||||
|
||||
[Authorize(Roles = "CanManageUsers")]
|
||||
[HttpGet("Users")]
|
||||
public async Task<ActionResult> UsersAsync([FromQuery] int page = 1, [FromQuery] int pageSize = 50)
|
||||
public async Task<ActionResult> UsersAsync(
|
||||
[FromQuery] int page = 1,
|
||||
[FromQuery] int pageSize = 50,
|
||||
[FromQuery] string? username = null,
|
||||
[FromQuery] string? title = null,
|
||||
[FromQuery] string? name = null,
|
||||
[FromQuery] string? surname = null,
|
||||
[FromQuery] string? workplace = null)
|
||||
{
|
||||
page = Math.Max(1, page);
|
||||
pageSize = Math.Clamp(pageSize, 10, 100);
|
||||
@@ -83,6 +90,29 @@ public class HomeController : Controller
|
||||
// Fetch all users with jpegPhoto (but not userPassword)
|
||||
IEnumerable<UserModel> allUsers = await _ldap.ListUsersAsync([.. _ldap.UsersAttributes.Where(attr => attr != "userPassword")]);
|
||||
List<UserModel> usersList = allUsers.ToList();
|
||||
|
||||
// Apply filters
|
||||
if (!string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
usersList = usersList.Where(u => u.Uid?.Contains(username, StringComparison.OrdinalIgnoreCase) == true).ToList();
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
usersList = usersList.Where(u => u.Title?.Contains(title, StringComparison.OrdinalIgnoreCase) == true).ToList();
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
usersList = usersList.Where(u => u.Cn?.Contains(name, StringComparison.OrdinalIgnoreCase) == true).ToList();
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(surname))
|
||||
{
|
||||
usersList = usersList.Where(u => u.Sn?.Contains(surname, StringComparison.OrdinalIgnoreCase) == true).ToList();
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(workplace))
|
||||
{
|
||||
usersList = usersList.Where(u => u.Description?.Workplace?.Contains(workplace, StringComparison.OrdinalIgnoreCase) == true).ToList();
|
||||
}
|
||||
|
||||
int totalUsers = usersList.Count;
|
||||
|
||||
List<UserModel> paginatedUsers = usersList
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createModal">
|
||||
@T["Create user"]
|
||||
</button>
|
||||
<div class="text-muted">
|
||||
<div class="text-muted" id="userCountInfo">
|
||||
<strong>@Model.TotalUsers</strong> @T["users"] @T["total"]
|
||||
@if (Model.TotalPages > 1)
|
||||
{
|
||||
@@ -28,8 +28,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* Filter Section *@
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@T["Filter Users"]</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-2">
|
||||
<input type="text" id="filterUsername" class="form-control form-control-sm" placeholder="@T["Username"]" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="text" id="filterTitle" class="form-control form-control-sm" placeholder="@T["Title"]" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="text" id="filterName" class="form-control form-control-sm" placeholder="@T["Name"]" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="text" id="filterSurname" class="form-control form-control-sm" placeholder="@T["Surname"]" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<input type="text" id="filterWorkplace" class="form-control form-control-sm" placeholder="@T["Workplace"]" />
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button id="clearFilters" class="btn btn-sm btn-secondary w-100">@T["Clear Filters"]</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<div id="loadingSpinner" class="text-center my-4" style="display: none;">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive" id="userTableContainer">
|
||||
<table class="table table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -42,7 +74,7 @@
|
||||
<th>@T["Action"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="userTableBody">
|
||||
@{
|
||||
foreach (UserTableViewModel userTableViewModel in Model.UserTableViewModels)
|
||||
{
|
||||
@@ -97,64 +129,224 @@
|
||||
</div>
|
||||
|
||||
@* Pagination Controls *@
|
||||
@if (Model.TotalPages > 1)
|
||||
{
|
||||
<nav aria-label="User pagination" class="mt-4">
|
||||
<nav aria-label="User pagination" class="mt-4" id="paginationContainer">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="text-muted">
|
||||
<div class="text-muted" id="paginationInfo">
|
||||
@T["Showing"] @((Model.CurrentPage - 1) * Model.PageSize + 1) - @Math.Min(Model.CurrentPage * Model.PageSize, Model.TotalUsers) @T["of"] @Model.TotalUsers @T["users"]
|
||||
</div>
|
||||
<ul class="pagination mb-0">
|
||||
@* Previous Button *@
|
||||
<li class="page-item @(Model.CurrentPage == 1 ? "disabled" : "")">
|
||||
<a class="page-link" href="?page=@(Model.CurrentPage - 1)&pageSize=@Model.PageSize" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@* Page Numbers *@
|
||||
@{
|
||||
int startPage = Math.Max(1, Model.CurrentPage - 2);
|
||||
int endPage = Math.Min(Model.TotalPages, Model.CurrentPage + 2);
|
||||
|
||||
if (startPage > 1)
|
||||
{
|
||||
<li class="page-item"><a class="page-link" href="?page=1&pageSize=@Model.PageSize">1</a></li>
|
||||
if (startPage > 2)
|
||||
{
|
||||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = startPage; i <= endPage; i++)
|
||||
{
|
||||
<li class="page-item @(i == Model.CurrentPage ? "active" : "")">
|
||||
<a class="page-link" href="?page=@i&pageSize=@Model.PageSize">@i</a>
|
||||
</li>
|
||||
}
|
||||
|
||||
if (endPage < Model.TotalPages)
|
||||
{
|
||||
if (endPage < Model.TotalPages - 1)
|
||||
{
|
||||
<li class="page-item disabled"><span class="page-link">...</span></li>
|
||||
}
|
||||
<li class="page-item"><a class="page-link" href="?page=@Model.TotalPages&pageSize=@Model.PageSize">@Model.TotalPages</a></li>
|
||||
}
|
||||
}
|
||||
|
||||
@* Next Button *@
|
||||
<li class="page-item @(Model.CurrentPage >= Model.TotalPages ? "disabled" : "")">
|
||||
<a class="page-link" href="?page=@(Model.CurrentPage + 1)&pageSize=@Model.PageSize" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
<ul class="pagination mb-0" id="paginationButtons">
|
||||
@* Pagination buttons will be generated dynamically via JavaScript *@
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Global state for pagination and filtering
|
||||
let currentPage = @Model.CurrentPage;
|
||||
let pageSize = @Model.PageSize;
|
||||
let totalUsers = @Model.TotalUsers;
|
||||
let totalPages = @Model.TotalPages;
|
||||
let currentFilters = {
|
||||
username: '',
|
||||
title: '',
|
||||
name: '',
|
||||
surname: '',
|
||||
workplace: ''
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize pagination
|
||||
renderPaginationButtons();
|
||||
|
||||
// Setup filter inputs with debounce
|
||||
const filterInputs = {
|
||||
username: document.getElementById('filterUsername'),
|
||||
title: document.getElementById('filterTitle'),
|
||||
name: document.getElementById('filterName'),
|
||||
surname: document.getElementById('filterSurname'),
|
||||
workplace: document.getElementById('filterWorkplace')
|
||||
};
|
||||
|
||||
let debounceTimer;
|
||||
Object.keys(filterInputs).forEach(key => {
|
||||
filterInputs[key].addEventListener('input', (e) => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
currentFilters[key] = e.target.value;
|
||||
currentPage = 1; // Reset to first page on filter change
|
||||
loadUsers();
|
||||
}, 500); // Wait 500ms after user stops typing
|
||||
});
|
||||
});
|
||||
|
||||
// Clear filters button
|
||||
document.getElementById('clearFilters').addEventListener('click', () => {
|
||||
Object.keys(filterInputs).forEach(key => {
|
||||
filterInputs[key].value = '';
|
||||
currentFilters[key] = '';
|
||||
});
|
||||
currentPage = 1;
|
||||
loadUsers();
|
||||
});
|
||||
});
|
||||
|
||||
async function loadUsers() {
|
||||
const spinner = document.getElementById('loadingSpinner');
|
||||
const tableContainer = document.getElementById('userTableContainer');
|
||||
|
||||
// Show spinner, hide table
|
||||
spinner.style.display = 'block';
|
||||
tableContainer.style.opacity = '0.5';
|
||||
|
||||
// Build query string
|
||||
const params = new URLSearchParams({
|
||||
page: currentPage,
|
||||
pageSize: pageSize
|
||||
});
|
||||
|
||||
Object.keys(currentFilters).forEach(key => {
|
||||
if (currentFilters[key]) {
|
||||
params.append(key, currentFilters[key]);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`/Home/Users?${params.toString()}`, {
|
||||
headers: {
|
||||
'Accept': 'text/html'
|
||||
}
|
||||
});
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
// Parse the response HTML to extract table rows and pagination info
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
// Extract table body
|
||||
const newTableBody = doc.querySelector('#userTableBody');
|
||||
if (newTableBody) {
|
||||
document.getElementById('userTableBody').innerHTML = newTableBody.innerHTML;
|
||||
|
||||
// Re-register row click handlers
|
||||
document.querySelectorAll('#userTableBody tr').forEach(row => {
|
||||
registerRowDetailviewClick(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Extract pagination info from the response
|
||||
const userCountInfo = doc.querySelector('#userCountInfo');
|
||||
if (userCountInfo) {
|
||||
const totalUsersMatch = userCountInfo.textContent.match(/(\d+)\s+users/i);
|
||||
if (totalUsersMatch) {
|
||||
totalUsers = parseInt(totalUsersMatch[1]);
|
||||
totalPages = Math.ceil(totalUsers / pageSize);
|
||||
|
||||
// Update count info
|
||||
document.getElementById('userCountInfo').innerHTML = userCountInfo.innerHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Update pagination info text
|
||||
const start = (currentPage - 1) * pageSize + 1;
|
||||
const end = Math.min(currentPage * pageSize, totalUsers);
|
||||
document.getElementById('paginationInfo').textContent =
|
||||
`@T["Showing"] ${start} - ${end} @T["of"] ${totalUsers} @T["users"]`;
|
||||
|
||||
// Render pagination buttons
|
||||
renderPaginationButtons();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading users:', error);
|
||||
showToast('@T["Error loading users"]', 'danger');
|
||||
} finally {
|
||||
// Hide spinner, restore table
|
||||
spinner.style.display = 'none';
|
||||
tableContainer.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
|
||||
function renderPaginationButtons() {
|
||||
const container = document.getElementById('paginationButtons');
|
||||
if (totalPages <= 1) {
|
||||
document.getElementById('paginationContainer').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('paginationContainer').style.display = 'block';
|
||||
container.innerHTML = '';
|
||||
|
||||
// Previous button
|
||||
const prevLi = document.createElement('li');
|
||||
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
|
||||
prevLi.innerHTML = `<a class="page-link" href="#" aria-label="Previous"><span aria-hidden="true">«</span></a>`;
|
||||
if (currentPage > 1) {
|
||||
prevLi.querySelector('a').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
currentPage--;
|
||||
loadUsers();
|
||||
});
|
||||
}
|
||||
container.appendChild(prevLi);
|
||||
|
||||
// Page numbers
|
||||
const startPage = Math.max(1, currentPage - 2);
|
||||
const endPage = Math.min(totalPages, currentPage + 2);
|
||||
|
||||
if (startPage > 1) {
|
||||
appendPageButton(container, 1);
|
||||
if (startPage > 2) {
|
||||
const ellipsis = document.createElement('li');
|
||||
ellipsis.className = 'page-item disabled';
|
||||
ellipsis.innerHTML = '<span class="page-link">...</span>';
|
||||
container.appendChild(ellipsis);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
appendPageButton(container, i);
|
||||
}
|
||||
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
const ellipsis = document.createElement('li');
|
||||
ellipsis.className = 'page-item disabled';
|
||||
ellipsis.innerHTML = '<span class="page-link">...</span>';
|
||||
container.appendChild(ellipsis);
|
||||
}
|
||||
appendPageButton(container, totalPages);
|
||||
}
|
||||
|
||||
// Next button
|
||||
const nextLi = document.createElement('li');
|
||||
nextLi.className = `page-item ${currentPage >= totalPages ? 'disabled' : ''}`;
|
||||
nextLi.innerHTML = `<a class="page-link" href="#" aria-label="Next"><span aria-hidden="true">»</span></a>`;
|
||||
if (currentPage < totalPages) {
|
||||
nextLi.querySelector('a').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
currentPage++;
|
||||
loadUsers();
|
||||
});
|
||||
}
|
||||
container.appendChild(nextLi);
|
||||
}
|
||||
|
||||
function appendPageButton(container, pageNum) {
|
||||
const li = document.createElement('li');
|
||||
li.className = `page-item ${pageNum === currentPage ? 'active' : ''}`;
|
||||
li.innerHTML = `<a class="page-link" href="#">${pageNum}</a>`;
|
||||
if (pageNum !== currentPage) {
|
||||
li.querySelector('a').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
currentPage = pageNum;
|
||||
loadUsers();
|
||||
});
|
||||
}
|
||||
container.appendChild(li);
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- User Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
|
||||
Reference in New Issue
Block a user