Fix user status toggle error

This commit is contained in:
2025-08-20 21:35:49 -04:00
parent dc600c2a8d
commit 2bc1e2ad81
2 changed files with 225 additions and 2277 deletions
File diff suppressed because it is too large Load Diff
+214 -164
View File
@@ -1,12 +1,11 @@
{% extends "base_authenticated.html" %} {% extends "base_authenticated.html" %} {% block title %}User Management - QR
{% block title %}User Management - QR Code Management{% endblock %} Code Management{% endblock %} {% block extra_head %}
{% block extra_head %}
<!-- Dedicated Users Page CSS --> <!-- Dedicated Users Page CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/users.css') }}"> <link
{% endblock %} rel="stylesheet"
href="{{ url_for('static', filename='css/users.css') }}"
{% block content %} />
{% endblock %} {% block content %}
<div class="users-page"> <div class="users-page">
<div class="users-header"> <div class="users-header">
<div class="header-content"> <div class="header-content">
@@ -32,7 +31,10 @@
<i class="fas fa-crown"></i> <i class="fas fa-crown"></i>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ users|selectattr('role', 'equalto', 'admin')|selectattr('active_status', 'equalto', True)|list|length }}</h3> <h3>
{{ users|selectattr('role', 'equalto',
'admin')|selectattr('active_status', 'equalto', True)|list|length }}
</h3>
<p>Active Administrators</p> <p>Active Administrators</p>
</div> </div>
</div> </div>
@@ -42,7 +44,10 @@
<i class="fas fa-users"></i> <i class="fas fa-users"></i>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ users|selectattr('role', 'equalto', 'staff')|selectattr('active_status', 'equalto', True)|list|length }}</h3> <h3>
{{ users|selectattr('role', 'equalto',
'staff')|selectattr('active_status', 'equalto', True)|list|length }}
</h3>
<p>Active Staff Members</p> <p>Active Staff Members</p>
</div> </div>
</div> </div>
@@ -52,7 +57,10 @@
<i class="fas fa-calculator"></i> <i class="fas fa-calculator"></i>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ users|selectattr('role', 'equalto', 'payroll')|selectattr('active_status', 'equalto', True)|list|length }}</h3> <h3>
{{ users|selectattr('role', 'equalto',
'payroll')|selectattr('active_status', 'equalto', True)|list|length }}
</h3>
<p>Payroll Specialists</p> <p>Payroll Specialists</p>
</div> </div>
</div> </div>
@@ -62,7 +70,11 @@
<i class="fas fa-project-diagram"></i> <i class="fas fa-project-diagram"></i>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ users|selectattr('role', 'equalto', 'project_manager')|selectattr('active_status', 'equalto', True)|list|length }}</h3> <h3>
{{ users|selectattr('role', 'equalto',
'project_manager')|selectattr('active_status', 'equalto',
True)|list|length }}
</h3>
<p>Project Managers</p> <p>Project Managers</p>
</div> </div>
</div> </div>
@@ -72,7 +84,9 @@
<i class="fas fa-user-check"></i> <i class="fas fa-user-check"></i>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ users|selectattr('active_status', 'equalto', True)|list|length }}</h3> <h3>
{{ users|selectattr('active_status', 'equalto', True)|list|length }}
</h3>
<p>Total Active Users</p> <p>Total Active Users</p>
</div> </div>
</div> </div>
@@ -82,7 +96,9 @@
<i class="fas fa-user-times"></i> <i class="fas fa-user-times"></i>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<h3>{{ users|selectattr('active_status', 'equalto', False)|list|length }}</h3> <h3>
{{ users|selectattr('active_status', 'equalto', False)|list|length }}
</h3>
<p>Inactive Users</p> <p>Inactive Users</p>
</div> </div>
</div> </div>
@@ -178,28 +194,27 @@
<td> <td>
<div class="user-role {{ user.role }}"> <div class="user-role {{ user.role }}">
{% if user.role == 'admin' %} {% if user.role == 'admin' %}
<i class="fas fa-crown"></i> <i class="fas fa-crown"></i>
Administrator Administrator {% elif user.role == 'staff' %}
{% elif user.role == 'staff' %} <i class="fas fa-user"></i>
<i class="fas fa-user"></i> Staff User {% elif user.role == 'payroll' %}
Staff User <i class="fas fa-calculator"></i>
{% elif user.role == 'payroll' %} Payroll Specialist {% elif user.role == 'project_manager' %}
<i class="fas fa-calculator"></i> <i class="fas fa-project-diagram"></i>
Payroll Specialist Project Manager {% else %}
{% elif user.role == 'project_manager' %} <i class="fas fa-user"></i>
<i class="fas fa-project-diagram"></i> {{ user.role.title() }} {% endif %}
Project Manager
{% else %}
<i class="fas fa-user"></i>
{{ user.role.title() }}
{% endif %}
</div> </div>
</td> </td>
<!-- Status --> <!-- Status -->
<td> <td>
<div class="user-status {{ 'active' if user.active_status else 'inactive' }}"> <div
<i class="fas fa-{{ 'check-circle' if user.active_status else 'times-circle' }}"></i> 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' }} {{ 'Active' if user.active_status else 'Inactive' }}
</div> </div>
</td> </td>
@@ -207,7 +222,10 @@
<!-- QR Codes Count --> <!-- QR Codes Count -->
<td class="user-qr-count"> <td class="user-qr-count">
<div class="qr-stats"> <div class="qr-stats">
<span class="qr-count">{{ user.created_qr_codes.filter_by(active_status=True).count() }}</span> <span class="qr-count"
>{{ user.created_qr_codes.filter_by(active_status=True).count()
}}</span
>
<small>QR Codes</small> <small>QR Codes</small>
</div> </div>
</td> </td>
@@ -215,24 +233,32 @@
<!-- Created Date --> <!-- Created Date -->
<td> <td>
{% if user.created_date %} {% if user.created_date %}
<div class="date-info"> <div class="date-info">
<div class="date-main">{{ user.created_date.strftime('%m/%d/%Y') }}</div> <div class="date-main">
<div class="date-time">{{ user.created_date.strftime('%I:%M %p') }}</div> {{ user.created_date.strftime('%m/%d/%Y') }}
</div> </div>
<div class="date-time">
{{ user.created_date.strftime('%I:%M %p') }}
</div>
</div>
{% else %} {% else %}
<span class="never-logged-in">Unknown</span> <span class="never-logged-in">Unknown</span>
{% endif %} {% endif %}
</td> </td>
<!-- Last Login --> <!-- Last Login -->
<td> <td>
{% if user.last_login_date %} {% if user.last_login_date %}
<div class="date-info"> <div class="date-info">
<div class="date-main">{{ user.last_login_date.strftime('%m/%d/%Y') }}</div> <div class="date-main">
<div class="date-time">{{ user.last_login_date.strftime('%I:%M %p') }}</div> {{ user.last_login_date.strftime('%m/%d/%Y') }}
</div> </div>
<div class="date-time">
{{ user.last_login_date.strftime('%I:%M %p') }}
</div>
</div>
{% else %} {% else %}
<span class="never-logged-in">Never</span> <span class="never-logged-in">Never</span>
{% endif %} {% endif %}
</td> </td>
@@ -240,50 +266,64 @@
<td class="user-actions"> <td class="user-actions">
<div class="action-buttons"> <div class="action-buttons">
<!-- Edit Button --> <!-- Edit Button -->
<a href="{{ url_for('edit_user', user_id=user.id) }}" <a
class="btn btn-sm btn-primary" href="{{ url_for('edit_user', user_id=user.id) }}"
title="Edit User"> class="btn btn-sm btn-primary"
title="Edit User"
>
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</a> </a>
{% if user.role != 'admin' %} {% if user.role != 'admin' %}
<!-- Promote to Admin --> <!-- Promote to Admin -->
<a href="{{ url_for('promote_user', user_id=user.id) }}" <a
class="btn btn-sm btn-success" href="{{ url_for('promote_user', user_id=user.id) }}"
title="Promote to Admin" class="btn btn-sm btn-success"
onclick="return confirm('Are you sure you want to promote {{ user.full_name }} to admin?')"> title="Promote to Admin"
<i class="fas fa-arrow-up"></i> onclick="return confirm('Are you sure you want to promote {{ user.full_name }} to admin?')"
</a> >
<i class="fas fa-arrow-up"></i>
</a>
{% else %} {% else %}
<!-- Demote from Admin --> <!-- Demote from Admin -->
{% if users|selectattr('role', 'equalto', 'admin')|selectattr('active_status', 'equalto', True)|list|length > 1 %} {% if users|selectattr('role', 'equalto',
<a href="{{ url_for('demote_user', user_id=user.id) }}" 'admin')|selectattr('active_status', 'equalto', True)|list|length
class="btn btn-sm btn-warning" > 1 %}
title="Demote from Admin" <a
onclick="return confirm('Are you sure you want to demote {{ user.full_name }} from admin?')"> href="{{ url_for('demote_user', user_id=user.id) }}"
<i class="fas fa-arrow-down"></i> class="btn btn-sm btn-warning"
</a> title="Demote from Admin"
{% else %} onclick="return confirm('Are you sure you want to demote {{ user.full_name }} from admin?')"
<button class="btn btn-sm btn-secondary" >
title="Cannot demote last admin" disabled> <i class="fas fa-arrow-down"></i>
<i class="fas fa-arrow-down"></i> </a>
</button> {% else %}
{% endif %} <button
{% endif %} class="btn btn-sm btn-secondary"
title="Cannot demote last admin"
disabled
>
<i class="fas fa-arrow-down"></i>
</button>
{% endif %} {% endif %}
<!-- Status Toggle --> <!-- Status Toggle -->
{% if user.active_status %} {% if user.active_status %}
<button class="btn btn-sm btn-danger" <button
title="Deactivate User" class="btn btn-sm btn-danger"
onclick="toggleUserStatus({{ user.id }}, false)"> title="Deactivate User"
<i class="fas fa-user-times"></i> onclick="toggleUserStatus({{ user.id }}, false)"
</button> >
<i class="fas fa-user-times"></i>
</button>
{% else %} {% else %}
<button class="btn btn-sm btn-success" <button
title="Activate User" class="btn btn-sm btn-success"
onclick="toggleUserStatus({{ user.id }}, true)"> title="Activate User"
<i class="fas fa-user-check"></i> onclick="toggleUserStatus({{ user.id }}, true)"
</button> >
<i class="fas fa-user-check"></i>
</button>
{% endif %} {% endif %}
</div> </div>
</td> </td>
@@ -308,15 +348,13 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %} {% block extra_scripts %}
{% block extra_scripts %}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener("DOMContentLoaded", function () {
const searchInput = document.getElementById('searchUsers'); const searchInput = document.getElementById("searchUsers");
const roleFilter = document.getElementById('roleFilter'); const roleFilter = document.getElementById("roleFilter");
const statusFilter = document.getElementById('statusFilter'); const statusFilter = document.getElementById("statusFilter");
const userRows = document.querySelectorAll('.user-row'); const userRows = document.querySelectorAll(".user-row");
// Search and filter functionality // Search and filter functionality
function filterUsers() { function filterUsers() {
@@ -324,14 +362,15 @@
const selectedRole = roleFilter.value; const selectedRole = roleFilter.value;
const selectedStatus = statusFilter.value; const selectedStatus = statusFilter.value;
userRows.forEach(row => { userRows.forEach((row) => {
const name = row.dataset.name || ''; const name = row.dataset.name || "";
const email = row.dataset.email || ''; const email = row.dataset.email || "";
const username = row.dataset.username || ''; const username = row.dataset.username || "";
const role = row.dataset.role || ''; const role = row.dataset.role || "";
const status = row.dataset.status || ''; const status = row.dataset.status || "";
const matchesSearch = !searchTerm || const matchesSearch =
!searchTerm ||
name.includes(searchTerm) || name.includes(searchTerm) ||
email.includes(searchTerm) || email.includes(searchTerm) ||
username.includes(searchTerm); username.includes(searchTerm);
@@ -340,9 +379,9 @@
const matchesStatus = !selectedStatus || status === selectedStatus; const matchesStatus = !selectedStatus || status === selectedStatus;
if (matchesSearch && matchesRole && matchesStatus) { if (matchesSearch && matchesRole && matchesStatus) {
row.style.display = ''; row.style.display = "";
} else { } else {
row.style.display = 'none'; row.style.display = "none";
} }
}); });
@@ -350,9 +389,11 @@
} }
function updateRowNumbers() { function updateRowNumbers() {
const visibleRows = Array.from(userRows).filter(row => row.style.display !== 'none'); const visibleRows = Array.from(userRows).filter(
(row) => row.style.display !== "none"
);
visibleRows.forEach((row, index) => { visibleRows.forEach((row, index) => {
const numberCell = row.querySelector('td:first-child'); const numberCell = row.querySelector("td:first-child");
if (numberCell) { if (numberCell) {
numberCell.textContent = index + 1; numberCell.textContent = index + 1;
} }
@@ -360,9 +401,9 @@
} }
// Event listeners // Event listeners
searchInput.addEventListener('input', filterUsers); searchInput.addEventListener("input", filterUsers);
roleFilter.addEventListener('change', filterUsers); roleFilter.addEventListener("change", filterUsers);
statusFilter.addEventListener('change', filterUsers); statusFilter.addEventListener("change", filterUsers);
// Initial filter on page load // Initial filter on page load
filterUsers(); filterUsers();
@@ -373,82 +414,91 @@
} }
function toggleUserStatus(userId, newStatus) { function toggleUserStatus(userId, newStatus) {
const action = newStatus ? 'activate' : 'deactivate'; const action = newStatus ? "activate" : "deactivate";
const message = `Are you sure you want to ${action} this user?`; const message = `Are you sure you want to ${action} this user?`;
if (confirm(message)) { if (confirm(message)) {
// Show loading state // Show loading state
const button = event.target.closest('button'); const button = event.target.closest("button");
const originalHTML = button.innerHTML; const originalHTML = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
button.disabled = true; button.disabled = true;
// Make AJAX call to toggle status // Make AJAX call to toggle status
fetch(`/users/${userId}/toggle_status`, { fetch(`/users/${userId}/toggle-status`, {
method: 'POST', method: "POST",
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
'X-Requested-With': 'XMLHttpRequest' "X-Requested-With": "XMLHttpRequest",
}, },
body: JSON.stringify({ body: JSON.stringify({
new_status: newStatus new_status: newStatus,
}) }),
}) })
.then(response => response.json()) .then((response) => response.json())
.then(data => { .then((data) => {
if (data.success) { if (data.success) {
// Update the UI immediately // Update the UI immediately
const userRow = button.closest('.user-row'); const userRow = button.closest(".user-row");
const statusCell = userRow.querySelector('.user-status'); const statusCell = userRow.querySelector(".user-status");
const statusToggleCell = userRow.querySelector('.user-actions'); const statusToggleCell = userRow.querySelector(".user-actions");
// Update status badge // Update status badge
if (data.new_status) { if (data.new_status) {
statusCell.innerHTML = '<div class="user-status active"><i class="fas fa-check-circle"></i> Active</div>'; 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 { } else {
statusCell.innerHTML = '<div class="user-status inactive"><i class="fas fa-times-circle"></i> Inactive</div>'; // Show error message
showNotification(data.message, "error");
button.innerHTML = originalHTML;
button.disabled = false;
} }
})
// Update the button .catch((error) => {
if (data.new_status) { console.error("Error:", error);
button.className = 'btn btn-sm btn-danger'; showNotification("Network error. Please try again.", "error");
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.innerHTML = originalHTML;
button.disabled = false; 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) { function showNotification(message, type) {
// Create notification element // Create notification element
const notification = document.createElement('div'); const notification = document.createElement("div");
notification.className = `alert alert-${type} notification`; notification.className = `alert alert-${type} notification`;
notification.innerHTML = ` notification.innerHTML = `
<i class="fas ${type === 'success' ? 'fa-check-circle' : 'fa-exclamation-circle'}"></i> <i class="fas ${
type === "success" ? "fa-check-circle" : "fa-exclamation-circle"
}"></i>
${message} ${message}
<button class="alert-close" onclick="this.parentElement.remove()"> <button class="alert-close" onclick="this.parentElement.remove()">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
@@ -460,33 +510,33 @@
// Style the notification // Style the notification
Object.assign(notification.style, { Object.assign(notification.style, {
position: 'fixed', position: "fixed",
top: '20px', top: "20px",
right: '20px', right: "20px",
zIndex: '9999', zIndex: "9999",
minWidth: '300px', minWidth: "300px",
padding: '12px 16px', padding: "12px 16px",
borderRadius: '8px', borderRadius: "8px",
boxShadow: '0 4px 12px rgba(0,0,0,0.15)', boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
backgroundColor: type === 'success' ? '#10b981' : '#ef4444', backgroundColor: type === "success" ? "#10b981" : "#ef4444",
color: 'white', color: "white",
display: 'flex', display: "flex",
alignItems: 'center', alignItems: "center",
gap: '8px', gap: "8px",
animation: 'slideInRight 0.3s ease-out' animation: "slideInRight 0.3s ease-out",
}); });
// Auto remove after 5 seconds // Auto remove after 5 seconds
setTimeout(() => { setTimeout(() => {
if (notification.parentElement) { if (notification.parentElement) {
notification.style.animation = 'slideOutRight 0.3s ease-out'; notification.style.animation = "slideOutRight 0.3s ease-out";
setTimeout(() => notification.remove(), 300); setTimeout(() => notification.remove(), 300);
} }
}, 5000); }, 5000);
} }
// Add CSS animations for notifications // Add CSS animations for notifications
const style = document.createElement('style'); const style = document.createElement("style");
style.textContent = ` style.textContent = `
@keyframes slideInRight { @keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; } from { transform: translateX(100%); opacity: 0; }