Files
LT_QR_Codes_Management/templates/users.html
T
2026-09-16 10:52:59 -04:00

594 lines
19 KiB
HTML

{% extends "base_authenticated.html" %} {% block title %}User Management - QR
Code Management{% endblock %} {% block extra_head %}
<!-- Dedicated Users Page CSS -->
<link
rel="stylesheet"
href="{{ url_for('static', filename='css/users.css') }}"
/>
{% endblock %} {% block content %}
<div class="users-page">
<div class="users-header">
<div class="header-content">
<h1>
<i class="fas fa-users"></i>
User Management
</h1>
<p>Manage system users and their access permissions</p>
</div>
<div class="header-actions">
<a href="{{ url_for('users.create_user') }}" class="btn btn-primary">
<i class="fas fa-user-plus"></i>
Add New User
</a>
</div>
</div>
<!-- User Statistics -->
<div class="user-stats">
<div class="stat-card admin">
<div class="stat-icon admin">
<i class="fas fa-crown"></i>
</div>
<div class="stat-info">
<h3>
{{ users|selectattr('role', 'equalto',
'admin')|selectattr('active_status', 'equalto', True)|list|length }}
</h3>
<p>Active Administrators</p>
</div>
</div>
<div class="stat-card staff">
<div class="stat-icon staff">
<i class="fas fa-users"></i>
</div>
<div class="stat-info">
<h3>
{{ users|selectattr('role', 'equalto',
'staff')|selectattr('active_status', 'equalto', True)|list|length }}
</h3>
<p>Active Staff Members</p>
</div>
</div>
<div class="stat-card payroll">
<div class="stat-icon payroll">
<i class="fas fa-calculator"></i>
</div>
<div class="stat-info">
<h3>
{{ users|selectattr('role', 'equalto',
'payroll')|selectattr('active_status', 'equalto', True)|list|length }}
</h3>
<p>Payroll Specialists</p>
</div>
</div>
<div class="stat-card accounting">
<div class="stat-icon accounting">
<i class="fas fa-file-invoice-dollar"></i>
</div>
<div class="stat-info">
<h3>
{{ users|selectattr('role', 'equalto',
'accounting')|selectattr('active_status', 'equalto', True)|list|length }}
</h3>
<p>Accounting Specialists</p>
</div>
</div>
<div class="stat-card project-manager">
<div class="stat-icon project-manager">
<i class="fas fa-project-diagram"></i>
</div>
<div class="stat-info">
<h3>
{{ users|selectattr('role', 'equalto',
'project_manager')|selectattr('active_status', 'equalto',
True)|list|length }}
</h3>
<p>Project Managers</p>
</div>
</div>
<div class="stat-card active">
<div class="stat-icon active">
<i class="fas fa-user-check"></i>
</div>
<div class="stat-info">
<h3>
{{ users|selectattr('active_status', 'equalto', True)|list|length }}
</h3>
<p>Total Active Users</p>
</div>
</div>
<div class="stat-card inactive">
<div class="stat-icon inactive">
<i class="fas fa-user-times"></i>
</div>
<div class="stat-info">
<h3>
{{ users|selectattr('active_status', 'equalto', False)|list|length }}
</h3>
<p>Inactive Users</p>
</div>
</div>
</div>
<!-- Users Controls -->
<div class="users-controls">
<div class="search-filters">
<div class="search-box">
<i class="fas fa-search"></i>
<input
type="text"
id="searchUsers"
placeholder="Search users by name, email, or username..."
/>
</div>
<div class="filter-group">
<select id="roleFilter" class="filter-select">
<option value="">All Roles</option>
<option value="admin">Administrators</option>
<option value="staff">Staff Members</option>
<option value="payroll">Payroll Specialists</option>
<option value="project_manager">Project Managers</option>
</select>
<select id="statusFilter" class="filter-select">
<option value="">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
</div>
<div class="view-options">
<button class="btn btn-secondary btn-sm" onclick="refreshUserList()">
<i class="fas fa-sync-alt"></i>
Refresh
</button>
</div>
</div>
<!-- Users Table -->
<div class="users-table-container">
<table class="users-table">
<thead>
<tr>
<th>#</th>
<th>User Information</th>
<th>Contact</th>
<th>Role</th>
<th>Status</th>
<th>QR Codes</th>
<th>Created</th>
<th>Last Login</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="usersTableBody">
{% for user in users %}
<tr
class="user-row"
data-name="{{ user.full_name.lower() }}"
data-email="{{ user.email.lower() }}"
data-username="{{ user.username.lower() }}"
data-role="{{ user.role }}"
data-status="{{ 'active' if user.active_status else 'inactive' }}"
>
<td>{{ loop.index }}</td>
<!-- User Information -->
<td>
<div class="user-info">
<div class="user-avatar">
<i class="fas fa-user-circle"></i>
</div>
<div class="user-details">
<h4>{{ user.full_name }}</h4>
<p>@{{ user.username }}</p>
</div>
</div>
</td>
<!-- Contact -->
<td class="user-contact">
<div class="contact-email">
<i class="fas fa-envelope"></i>
{{ user.email }}
</div>
</td>
<!-- Role -->
<td>
<div class="user-role {{ user.role }}">
{% if user.role == 'admin' %}
<i class="fas fa-crown"></i>
Administrator {% elif user.role == 'staff' %}
<i class="fas fa-user"></i>
Staff User {% elif user.role == 'payroll' %}
<i class="fas fa-calculator"></i>
Payroll Specialist {% elif user.role == 'project_manager' %}
<i class="fas fa-project-diagram"></i>
Project Manager {% else %}
<i class="fas fa-user"></i>
{{ user.role.title() }} {% endif %}
</div>
</td>
<!-- Status -->
<td>
<div
class="user-status {{ 'active' if user.active_status else 'inactive' }}"
>
<i
class="fas fa-{{ 'check-circle' if user.active_status else 'times-circle' }}"
></i>
{{ 'Active' if user.active_status else 'Inactive' }}
</div>
</td>
<!-- QR Codes Count -->
<td class="user-qr-count">
<div class="qr-stats">
<span class="qr-count"
>{{ user.created_qr_codes.filter_by(active_status=True).count()
}}</span
>
<small>QR Codes</small>
</div>
</td>
<!-- Created Date -->
<td>
{% if user.created_date %}
<div class="date-info">
<div class="date-main">
{{ user.created_date.strftime('%m/%d/%Y') }}
</div>
<div class="date-time">
{{ user.created_date.strftime('%I:%M %p') }}
</div>
</div>
{% else %}
<span class="never-logged-in">Unknown</span>
{% endif %}
</td>
<!-- Last Login -->
<td>
{% if user.last_login_date %}
<div class="date-info">
<div class="date-main">
{{ user.last_login_date.strftime('%m/%d/%Y') }}
</div>
<div class="date-time">
{{ user.last_login_date.strftime('%I:%M %p') }}
</div>
</div>
{% else %}
<span class="never-logged-in">Never</span>
{% endif %}
</td>
<!-- Actions -->
<td class="user-actions">
<div class="action-buttons">
<!-- Edit Button -->
<a
href="{{ url_for('users.edit_user', user_id=user.id) }}"
class="btn btn-sm btn-primary"
title="Edit User"
>
<i class="fas fa-edit"></i>
</a>
{% if user.role != 'admin' %}
<!-- Promote to Admin -->
<form
method="POST"
action="{{ url_for('users.promote_user', user_id=user.id) }}"
style="display: inline; margin: 0;"
data-confirm="Are you sure you want to promote {{ user.full_name }} to admin?"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-success" title="Promote to Admin">
<i class="fas fa-arrow-up"></i>
</button>
</form>
{% else %}
<!-- Demote from Admin -->
{% if users|selectattr('role', 'equalto',
'admin')|selectattr('active_status', 'equalto', True)|list|length
> 1 %}
<form
method="POST"
action="{{ url_for('users.demote_user', user_id=user.id) }}"
style="display: inline; margin: 0;"
data-confirm="Are you sure you want to demote {{ user.full_name }} from admin?"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-warning" title="Demote from Admin">
<i class="fas fa-arrow-down"></i>
</button>
</form>
{% else %}
<button
class="btn btn-sm btn-secondary"
title="Cannot demote last admin"
disabled
>
<i class="fas fa-arrow-down"></i>
</button>
{% endif %} {% endif %}
<!-- Status Toggle -->
{% if user.active_status %}
<button
class="btn btn-sm btn-danger"
title="Deactivate User"
onclick="toggleUserStatus({{ user.id }}, false)"
>
<i class="fas fa-user-times"></i>
</button>
{% else %}
<button
class="btn btn-sm btn-success"
title="Activate User"
onclick="toggleUserStatus({{ user.id }}, true)"
>
<i class="fas fa-user-check"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- No Users Message -->
{% if not users %}
<div class="no-users-message">
<div class="no-users-content">
<i class="fas fa-users"></i>
<h3>No Users Found</h3>
<p>There are currently no users in the system.</p>
<a href="{{ url_for('users.create_user') }}" class="btn btn-primary">
<i class="fas fa-user-plus"></i>
Create First User
</a>
</div>
</div>
{% endif %}
</div>
{% endblock %} {% block extra_scripts %}
<script>
document.addEventListener("DOMContentLoaded", function () {
const searchInput = document.getElementById("searchUsers");
const roleFilter = document.getElementById("roleFilter");
const statusFilter = document.getElementById("statusFilter");
const userRows = document.querySelectorAll(".user-row");
// Search and filter functionality
function filterUsers() {
const searchTerm = searchInput.value.toLowerCase();
const selectedRole = roleFilter.value;
const selectedStatus = statusFilter.value;
userRows.forEach((row) => {
const name = row.dataset.name || "";
const email = row.dataset.email || "";
const username = row.dataset.username || "";
const role = row.dataset.role || "";
const status = row.dataset.status || "";
const matchesSearch =
!searchTerm ||
name.includes(searchTerm) ||
email.includes(searchTerm) ||
username.includes(searchTerm);
const matchesRole = !selectedRole || role === selectedRole;
const matchesStatus = !selectedStatus || status === selectedStatus;
if (matchesSearch && matchesRole && matchesStatus) {
row.style.display = "";
} else {
row.style.display = "none";
}
});
updateRowNumbers();
}
function updateRowNumbers() {
const visibleRows = Array.from(userRows).filter(
(row) => row.style.display !== "none"
);
visibleRows.forEach((row, index) => {
const numberCell = row.querySelector("td:first-child");
if (numberCell) {
numberCell.textContent = index + 1;
}
});
}
// Event listeners
searchInput.addEventListener("input", filterUsers);
roleFilter.addEventListener("change", filterUsers);
statusFilter.addEventListener("change", filterUsers);
// Initial filter on page load
filterUsers();
});
function refreshUserList() {
window.location.reload();
}
// Promote / demote are POST forms carrying the CSRF token (they used to be
// GET links, which a forged link could trigger). Confirm before submitting.
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll("form[data-confirm]").forEach(function (form) {
form.addEventListener("submit", function (event) {
if (!confirm(form.dataset.confirm)) {
event.preventDefault();
}
});
});
});
function toggleUserStatus(userId, newStatus) {
const action = newStatus ? "activate" : "deactivate";
const message = `Are you sure you want to ${action} this user?`;
if (confirm(message)) {
// Show loading state
const button = event.target.closest("button");
const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
button.disabled = true;
// Make AJAX call to toggle status
fetch(`/users/${userId}/toggle-status`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": (window.qrConfig && window.qrConfig.csrfToken) || "",
"X-Requested-With": "XMLHttpRequest",
"X-CSRF-Token": (window.qrConfig && window.qrConfig.csrfToken) || "",
},
body: JSON.stringify({
new_status: newStatus,
}),
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
// Update the UI immediately
const userRow = button.closest(".user-row");
const statusCell = userRow.querySelector(".user-status");
const statusToggleCell = userRow.querySelector(".user-actions");
// Update status badge
if (data.new_status) {
statusCell.innerHTML =
'<div class="user-status active"><i class="fas fa-check-circle"></i> Active</div>';
} else {
statusCell.innerHTML =
'<div class="user-status inactive"><i class="fas fa-times-circle"></i> Inactive</div>';
}
// Update the button
if (data.new_status) {
button.className = "btn btn-sm btn-danger";
button.title = "Deactivate User";
button.setAttribute(
"onclick",
`toggleUserStatus(${userId}, false)`
);
button.innerHTML = '<i class="fas fa-user-times"></i>';
} else {
button.className = "btn btn-sm btn-success";
button.title = "Activate User";
button.setAttribute(
"onclick",
`toggleUserStatus(${userId}, true)`
);
button.innerHTML = '<i class="fas fa-user-check"></i>';
}
button.disabled = false;
// Show success message
showNotification(data.message, "success");
} else {
// Show error message
showNotification(data.message, "error");
button.innerHTML = originalHTML;
button.disabled = false;
}
})
.catch((error) => {
console.error("Error:", error);
showNotification("Network error. Please try again.", "error");
button.innerHTML = originalHTML;
button.disabled = false;
});
}
}
function showNotification(message, type) {
// Create notification element
const notification = document.createElement("div");
notification.className = `alert alert-${type} notification`;
notification.innerHTML = `
<i class="fas ${
type === "success" ? "fa-check-circle" : "fa-exclamation-circle"
}"></i>
${message}
<button class="alert-close" onclick="this.parentElement.remove()">
<i class="fas fa-times"></i>
</button>
`;
// Add notification to page
document.body.appendChild(notification);
// Style the notification
Object.assign(notification.style, {
position: "fixed",
top: "20px",
right: "20px",
zIndex: "9999",
minWidth: "300px",
padding: "12px 16px",
borderRadius: "8px",
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
backgroundColor: type === "success" ? "#10b981" : "#ef4444",
color: "white",
display: "flex",
alignItems: "center",
gap: "8px",
animation: "slideInRight 0.3s ease-out",
});
// Auto remove after 5 seconds
setTimeout(() => {
if (notification.parentElement) {
notification.style.animation = "slideOutRight 0.3s ease-out";
setTimeout(() => notification.remove(), 300);
}
}, 5000);
}
// Add CSS animations for notifications
const style = document.createElement("style");
style.textContent = `
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOutRight {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
.alert-close {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 0;
margin-left: auto;
}
`;
document.head.appendChild(style);
</script>
{% endblock %}