🎯 RESTORE: Enhanced admin user management functionality
✅ Added user search functionality to admin users page ✅ Created comprehensive user detail template with: - Complete user information display - User action buttons (activate/deactivate/delete) - Quotation history for each user - Improved tooltips and responsive design ✅ Enhanced admin users table with better action buttons ✅ Added proper error handling and confirmation dialogs SAFE: Only affects admin pages, calculator functionality unchanged
This commit is contained in:
+16
-2
@@ -57,9 +57,23 @@ def dashboard():
|
||||
def users():
|
||||
try:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
search = request.args.get('search', '').strip()
|
||||
per_page = 20
|
||||
|
||||
users = User.query.order_by(User.created_at.desc()).paginate(
|
||||
# Build query with search functionality
|
||||
query = User.query
|
||||
|
||||
if search:
|
||||
# Search by username or email (case-insensitive)
|
||||
search_filter = f"%{search.lower()}%"
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
User.username.ilike(search_filter),
|
||||
User.email.ilike(search_filter)
|
||||
)
|
||||
)
|
||||
|
||||
users = query.order_by(User.created_at.desc()).paginate(
|
||||
page=page, per_page=per_page, error_out=False
|
||||
)
|
||||
|
||||
@@ -229,7 +243,7 @@ def delete_quotation():
|
||||
if not request.is_json:
|
||||
return jsonify({'error': 'Invalid request format'}), 400
|
||||
|
||||
data = request.get_json()
|
||||
data = request.get_json()
|
||||
quotation_id = data.get('quotation_id')
|
||||
|
||||
if not quotation_id:
|
||||
|
||||
@@ -1 +1,276 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}User Details - {{ user.username }} - Admin Panel{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>
|
||||
<i class="fas fa-user me-2 text-primary"></i>
|
||||
User Details: {{ user.username }}
|
||||
</h2>
|
||||
<a href="{{ url_for('admin.users') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Back to Users
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- User Information Card -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-transparent border-0">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-info-circle me-2 text-primary"></i>
|
||||
User Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">User ID:</label>
|
||||
<span class="badge bg-secondary">#{{ user.id }}</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Username:</label>
|
||||
<span class="text-muted">{{ user.username }}</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Email:</label>
|
||||
<span class="text-muted">{{ user.email }}</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Company:</label>
|
||||
<span class="text-muted">{{ user.company if user.company else 'Not specified' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Status:</label>
|
||||
<span class="badge bg-{{ 'success' if user.is_active else 'danger' }}">
|
||||
<i class="fas fa-{{ 'check-circle' if user.is_active else 'times-circle' }} me-1"></i>
|
||||
{{ 'Active' if user.is_active else 'Inactive' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Role:</label>
|
||||
<span class="badge bg-{{ 'primary' if user.is_admin else 'secondary' }}">
|
||||
<i class="fas fa-{{ 'crown' if user.is_admin else 'user' }} me-1"></i>
|
||||
{{ 'Admin' if user.is_admin else 'User' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Last Login:</label>
|
||||
<span class="text-muted">
|
||||
{{ user.last_login.strftime('%B %d, %Y at %H:%M') if user.last_login else 'Never' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Account Created:</label>
|
||||
<span class="text-muted">{{ user.created_at.strftime('%B %d, %Y at %H:%M') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Actions Card -->
|
||||
<div class="card border-0 shadow-sm mb-4">
|
||||
<div class="card-header bg-transparent border-0">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-cogs me-2 text-primary"></i>
|
||||
User Actions
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button"
|
||||
class="btn btn-{{ 'warning' if user.is_active else 'success' }} toggle-user-btn"
|
||||
data-user-id="{{ user.id }}"
|
||||
data-is-active="{{ user.is_active|tojson }}"
|
||||
title="{{ 'Deactivate' if user.is_active else 'Activate' }} User">
|
||||
<i class="fas fa-{{ 'ban' if user.is_active else 'check' }} me-2"></i>
|
||||
{{ 'Deactivate' if user.is_active else 'Activate' }} User
|
||||
</button>
|
||||
{% if user.id != current_user.id %}
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger delete-user-btn"
|
||||
data-user-id="{{ user.id }}"
|
||||
title="Delete User">
|
||||
<i class="fas fa-trash me-2"></i>Delete User
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quotations History Card -->
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-transparent border-0">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="fas fa-file-invoice me-2 text-primary"></i>
|
||||
Quotation History ({{ quotations|length }} total)
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if quotations %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="border-0">ID</th>
|
||||
<th class="border-0">Project Name</th>
|
||||
<th class="border-0">Mold Type</th>
|
||||
<th class="border-0">Country</th>
|
||||
<th class="border-0">Total Cost</th>
|
||||
<th class="border-0">Created</th>
|
||||
<th class="border-0 text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for quotation in quotations %}
|
||||
<tr>
|
||||
<td>
|
||||
<span class="badge bg-info">#{{ quotation.id }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-medium">{{ quotation.project_name or 'Unnamed Project' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted">{{ quotation.mold_main_type or 'Not specified' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-muted">{{ quotation.mold_shop_country or 'Not specified' }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-bold text-success">
|
||||
${{ "{:,.2f}".format(quotation.total_cost) if quotation.total_cost else 'N/A' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{{ quotation.created_at.strftime('%m/%d/%Y %H:%M') }}
|
||||
</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a href="{{ url_for('admin.quotation_detail', id=quotation.id) }}"
|
||||
class="btn btn-outline-info btn-sm"
|
||||
title="View Quotation Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-file-invoice fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">No quotations found</h5>
|
||||
<p class="text-muted">This user hasn't created any quotations yet.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Toggle user status
|
||||
document.querySelectorAll('.toggle-user-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const userId = this.dataset.userId;
|
||||
const isActive = this.dataset.isActive === 'true';
|
||||
const action = isActive ? 'deactivate' : 'activate';
|
||||
|
||||
if (!confirm(`Are you sure you want to ${action} this user?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch(`/admin/user/${userId}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred while updating user status');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Delete user
|
||||
document.querySelectorAll('.delete-user-btn').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const userId = this.dataset.userId;
|
||||
|
||||
if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/admin/delete_user', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify({ user_id: parseInt(userId) })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.message) {
|
||||
alert('User deleted successfully');
|
||||
window.location.href = '/admin/users';
|
||||
} else {
|
||||
alert('Error: ' + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('An error occurred while deleting the user');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block styles %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
.form-label {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -85,21 +85,27 @@
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
|
||||
class="btn btn-outline-info btn-sm"
|
||||
title="View Details">
|
||||
title="View User Details"
|
||||
data-bs-toggle="tooltip">
|
||||
<i class="fas fa-eye"></i>
|
||||
<span class="d-none d-md-inline ms-1">View</span>
|
||||
</a>
|
||||
<button type="button"
|
||||
class="btn btn-{{ 'warning' if user.is_active else 'success' }} btn-sm"
|
||||
onclick="toggleUserStatus({{ user.id }}, '{{ 'warning' if user.is_active else 'success' }}')"
|
||||
title="{{ 'Deactivate' if user.is_active else 'Activate' }} User">
|
||||
title="{{ 'Deactivate User' if user.is_active else 'Activate User' }}"
|
||||
data-bs-toggle="tooltip">
|
||||
<i class="fas fa-{{ 'ban' if user.is_active else 'check' }}"></i>
|
||||
<span class="d-none d-md-inline ms-1">{{ 'Deactivate' if user.is_active else 'Activate' }}</span>
|
||||
</button>
|
||||
{% if user.id != current_user.id %}
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
onclick="deleteUser({{ user.id }})"
|
||||
title="Delete User">
|
||||
title="Delete User Permanently"
|
||||
data-bs-toggle="tooltip">
|
||||
<i class="fas fa-trash"></i>
|
||||
<span class="d-none d-md-inline ms-1">Delete</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -164,6 +170,14 @@
|
||||
{% block scripts %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
// Initialize tooltips
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
||||
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
|
||||
return new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
});
|
||||
|
||||
function toggleUserStatus(userId, status) {
|
||||
const button = event.target.closest('button');
|
||||
const isActive = status === 'warning';
|
||||
|
||||
Reference in New Issue
Block a user