Updated activate/deactivate user function

This commit is contained in:
Nguyen Ngo
2025-08-11 10:58:52 -04:00
parent 413d1dac73
commit 26b6fa3871
2 changed files with 356 additions and 152 deletions
+126 -3
View File
@@ -1506,18 +1506,141 @@ def edit_user(user_id):
flash('Error updating user. Please try again.', 'error')
return redirect(url_for('users'))
@app.route('/users/<int:user_id>/toggle_status', methods=['POST'])
@admin_required
def toggle_user_status(user_id):
"""Toggle user active status (Admin only)"""
try:
user_to_toggle = User.query.get(user_id)
current_user = User.query.get(session['user_id'])
if not user_to_toggle:
return jsonify({
'success': False,
'message': 'User not found.'
}), 404
# Prevent self-deactivation
if user_to_toggle.id == current_user.id:
return jsonify({
'success': False,
'message': 'You cannot deactivate yourself.'
}), 400
# Check if this is the last admin being deactivated
if (user_to_toggle.role == 'admin' and user_to_toggle.active_status == True):
active_admin_count = User.query.filter_by(role='admin', active_status=True).count()
if active_admin_count <= 1:
return jsonify({
'success': False,
'message': 'Cannot deactivate the last admin user.'
}), 400
# Toggle the status
new_status = not user_to_toggle.active_status
user_to_toggle.active_status = new_status
db.session.commit()
action = 'activated' if new_status else 'deactivated'
message = f'"{user_to_toggle.full_name}" has been {action} successfully.'
print(f"Admin {current_user.username} {action} user {user_to_toggle.username}")
return jsonify({
'success': True,
'message': message,
'new_status': new_status,
'user_id': user_id
})
except Exception as e:
db.session.rollback()
print(f"Error toggling user status: {e}")
return jsonify({
'success': False,
'message': 'Error updating user status. Please try again.'
}), 500
@app.route('/users/<int:user_id>/activate', methods=['GET', 'POST'])
@admin_required
def activate_user(user_id):
"""Activate a user (Admin only) - Alternative route"""
try:
user_to_activate = User.query.get(user_id)
current_user = User.query.get(session['user_id'])
if not user_to_activate:
flash('User not found.', 'error')
return redirect(url_for('users'))
if user_to_activate.active_status:
flash('User is already active.', 'info')
else:
user_to_activate.active_status = True
db.session.commit()
flash(f'"{user_to_activate.full_name}" has been activated.', 'success')
print(f"Admin {current_user.username} activated user {user_to_activate.username}")
return redirect(url_for('users'))
except Exception as e:
db.session.rollback()
print(f"Error activating user: {e}")
flash('Error activating user. Please try again.', 'error')
return redirect(url_for('users'))
@app.route('/users/<int:user_id>/deactivate', methods=['GET', 'POST'])
@admin_required
def deactivate_user(user_id):
"""Deactivate a user (Admin only) - Alternative route"""
try:
user_to_deactivate = User.query.get(user_id)
current_user = User.query.get(session['user_id'])
if not user_to_deactivate:
flash('User not found.', 'error')
return redirect(url_for('users'))
# Prevent self-deactivation
if user_to_deactivate.id == current_user.id:
flash('You cannot deactivate yourself.', 'error')
return redirect(url_for('users'))
# Check if this is the last admin
if user_to_deactivate.role == 'admin' and user_to_deactivate.active_status:
active_admin_count = User.query.filter_by(role='admin', active_status=True).count()
if active_admin_count <= 1:
flash('Cannot deactivate the last admin user.', 'error')
return redirect(url_for('users'))
if not user_to_deactivate.active_status:
flash('User is already deactivated.', 'info')
else:
user_to_deactivate.active_status = False
db.session.commit()
flash(f'"{user_to_deactivate.full_name}" has been deactivated.', 'success')
print(f"Admin {current_user.username} deactivated user {user_to_deactivate.username}")
return redirect(url_for('users'))
except Exception as e:
db.session.rollback()
print(f"Error deactivating user: {e}")
flash('Error deactivating user. Please try again.', 'error')
return redirect(url_for('users'))
# ENHANCED USER STATISTICS API
@app.route('/api/users/stats')
@admin_required
def user_stats_api():
"""API endpoint for user statistics"""
payroll_users = User.query.filter_by(role='payroll', active_status=True).count()
project_manager_users = User.query.filter_by(role='project_manager', active_status=True).count()
"""UPDATED: API endpoint for user statistics - includes new roles"""
try:
total_users = User.query.count()
active_users = User.query.filter_by(active_status=True).count()
admin_users = User.query.filter_by(role='admin', active_status=True).count()
staff_users = User.query.filter_by(role='staff', active_status=True).count()
payroll_users = User.query.filter_by(role='payroll', active_status=True).count()
project_manager_users = User.query.filter_by(role='project_manager', active_status=True).count()
inactive_users = User.query.filter_by(active_status=False).count()
# Recent registrations (last 30 days)
+196 -115
View File
@@ -1,11 +1,12 @@
{% extends "base_authenticated.html" %} {% block title %}User Management - QR
Code Management{% endblock %} {% block extra_head %}
{% 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 %}
<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">
@@ -31,10 +32,7 @@ Code Management{% endblock %} {% block extra_head %}
<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>
<h3>{{ users|selectattr('role', 'equalto', 'admin')|selectattr('active_status', 'equalto', True)|list|length }}</h3>
<p>Active Administrators</p>
</div>
</div>
@@ -44,10 +42,7 @@ Code Management{% endblock %} {% block extra_head %}
<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>
<h3>{{ users|selectattr('role', 'equalto', 'staff')|selectattr('active_status', 'equalto', True)|list|length }}</h3>
<p>Active Staff Members</p>
</div>
</div>
@@ -57,10 +52,7 @@ Code Management{% endblock %} {% block extra_head %}
<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>
<h3>{{ users|selectattr('role', 'equalto', 'payroll')|selectattr('active_status', 'equalto', True)|list|length }}</h3>
<p>Payroll Specialists</p>
</div>
</div>
@@ -70,11 +62,7 @@ Code Management{% endblock %} {% block extra_head %}
<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>
<h3>{{ users|selectattr('role', 'equalto', 'project_manager')|selectattr('active_status', 'equalto', True)|list|length }}</h3>
<p>Project Managers</p>
</div>
</div>
@@ -84,9 +72,7 @@ Code Management{% endblock %} {% block extra_head %}
<i class="fas fa-user-check"></i>
</div>
<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>
</div>
</div>
@@ -96,9 +82,7 @@ Code Management{% endblock %} {% block extra_head %}
<i class="fas fa-user-times"></i>
</div>
<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>
</div>
</div>
@@ -195,26 +179,27 @@ Code Management{% endblock %} {% block extra_head %}
<div class="user-role {{ user.role }}">
{% if user.role == 'admin' %}
<i class="fas fa-crown"></i>
Administrator {% elif user.role == 'staff' %}
Administrator
{% elif user.role == 'staff' %}
<i class="fas fa-user"></i>
Staff User {% elif user.role == 'payroll' %}
Staff User
{% elif user.role == 'payroll' %}
<i class="fas fa-calculator"></i>
Payroll Specialist {% elif user.role == 'project_manager' %}
Payroll Specialist
{% elif user.role == 'project_manager' %}
<i class="fas fa-project-diagram"></i>
Project Manager {% else %}
Project Manager
{% else %}
<i class="fas fa-user"></i>
{{ user.role.title() }} {% endif %}
{{ 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>
<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>
@@ -222,10 +207,7 @@ Code Management{% endblock %} {% block extra_head %}
<!-- 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
>
<span class="qr-count">{{ user.created_qr_codes.filter_by(active_status=True).count() }}</span>
<small>QR Codes</small>
</div>
</td>
@@ -234,12 +216,8 @@ Code Management{% endblock %} {% block extra_head %}
<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 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>
@@ -250,12 +228,8 @@ Code Management{% endblock %} {% block extra_head %}
<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 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>
@@ -266,62 +240,48 @@ Code Management{% endblock %} {% block extra_head %}
<td class="user-actions">
<div class="action-buttons">
<!-- Edit Button -->
<a
href="{{ url_for('edit_user', user_id=user.id) }}"
<a href="{{ url_for('edit_user', user_id=user.id) }}"
class="btn btn-sm btn-primary"
title="Edit User"
>
title="Edit User">
<i class="fas fa-edit"></i>
</a>
{% if user.role != 'admin' %}
<!-- Promote to Admin -->
<a
href="{{ url_for('promote_user', user_id=user.id) }}"
<a href="{{ url_for('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?')"
>
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('demote_user', user_id=user.id) }}"
{% if users|selectattr('role', 'equalto', 'admin')|selectattr('active_status', 'equalto', True)|list|length > 1 %}
<a href="{{ url_for('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?')"
>
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
>
<button class="btn btn-sm btn-secondary"
title="Cannot demote last admin" disabled>
<i class="fas fa-arrow-down"></i>
</button>
{% endif %} {% endif %}
{% endif %}
{% endif %}
<!-- Status Toggle -->
{% if user.active_status %}
<button
class="btn btn-sm btn-danger"
<button class="btn btn-sm btn-danger"
title="Deactivate User"
onclick="toggleUserStatus({{ user.id }}, false)"
>
onclick="toggleUserStatus({{ user.id }}, false)">
<i class="fas fa-user-times"></i>
</button>
{% else %}
<button
class="btn btn-sm btn-success"
<button class="btn btn-sm btn-success"
title="Activate User"
onclick="toggleUserStatus({{ user.id }}, true)"
>
onclick="toggleUserStatus({{ user.id }}, true)">
<i class="fas fa-user-check"></i>
</button>
{% endif %}
@@ -348,13 +308,15 @@ Code Management{% endblock %} {% block extra_head %}
</div>
{% endif %}
</div>
{% endblock %} {% block extra_scripts %}
{% 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");
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() {
@@ -362,15 +324,14 @@ Code Management{% endblock %} {% block extra_head %}
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 || "";
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 ||
const matchesSearch = !searchTerm ||
name.includes(searchTerm) ||
email.includes(searchTerm) ||
username.includes(searchTerm);
@@ -379,9 +340,9 @@ Code Management{% endblock %} {% block extra_head %}
const matchesStatus = !selectedStatus || status === selectedStatus;
if (matchesSearch && matchesRole && matchesStatus) {
row.style.display = "";
row.style.display = '';
} else {
row.style.display = "none";
row.style.display = 'none';
}
});
@@ -389,11 +350,9 @@ Code Management{% endblock %} {% block extra_head %}
}
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) => {
const numberCell = row.querySelector("td:first-child");
const numberCell = row.querySelector('td:first-child');
if (numberCell) {
numberCell.textContent = index + 1;
}
@@ -401,9 +360,9 @@ Code Management{% endblock %} {% block extra_head %}
}
// Event listeners
searchInput.addEventListener("input", filterUsers);
roleFilter.addEventListener("change", filterUsers);
statusFilter.addEventListener("change", filterUsers);
searchInput.addEventListener('input', filterUsers);
roleFilter.addEventListener('change', filterUsers);
statusFilter.addEventListener('change', filterUsers);
// Initial filter on page load
filterUsers();
@@ -414,16 +373,138 @@ Code Management{% endblock %} {% block extra_head %}
}
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?`;
if (confirm(message)) {
// Here you would implement the actual status toggle
// For now, just reload the page
// In a full implementation, you'd make an AJAX call to update the status
alert(`User ${action}d successfully!`);
window.location.reload();
// 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-Requested-With': 'XMLHttpRequest'
},
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 %}