576 lines
18 KiB
HTML
576 lines
18 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 -->
|
|
<a
|
|
href="{{ url_for('users.promote_user', user_id=user.id) }}"
|
|
class="btn btn-sm btn-success"
|
|
title="Promote to Admin"
|
|
onclick="return confirm('Are you sure you want to promote {{ user.full_name }} to admin?')"
|
|
>
|
|
<i class="fas fa-arrow-up"></i>
|
|
</a>
|
|
{% else %}
|
|
<!-- Demote from Admin -->
|
|
{% if users|selectattr('role', 'equalto',
|
|
'admin')|selectattr('active_status', 'equalto', True)|list|length
|
|
> 1 %}
|
|
<a
|
|
href="{{ url_for('users.demote_user', user_id=user.id) }}"
|
|
class="btn btn-sm btn-warning"
|
|
title="Demote from Admin"
|
|
onclick="return confirm('Are you sure you want to demote {{ user.full_name }} from admin?')"
|
|
>
|
|
<i class="fas fa-arrow-down"></i>
|
|
</a>
|
|
{% 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();
|
|
}
|
|
|
|
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 %}
|