🎯 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():
|
def users():
|
||||||
try:
|
try:
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
|
search = request.args.get('search', '').strip()
|
||||||
per_page = 20
|
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
|
page=page, per_page=per_page, error_out=False
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -229,7 +243,7 @@ def delete_quotation():
|
|||||||
if not request.is_json:
|
if not request.is_json:
|
||||||
return jsonify({'error': 'Invalid request format'}), 400
|
return jsonify({'error': 'Invalid request format'}), 400
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
quotation_id = data.get('quotation_id')
|
quotation_id = data.get('quotation_id')
|
||||||
|
|
||||||
if not 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">
|
<div class="btn-group" role="group">
|
||||||
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
|
<a href="{{ url_for('admin.user_detail', user_id=user.id) }}"
|
||||||
class="btn btn-outline-info btn-sm"
|
class="btn btn-outline-info btn-sm"
|
||||||
title="View Details">
|
title="View User Details"
|
||||||
|
data-bs-toggle="tooltip">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
|
<span class="d-none d-md-inline ms-1">View</span>
|
||||||
</a>
|
</a>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-{{ 'warning' if user.is_active else 'success' }} btn-sm"
|
class="btn btn-{{ 'warning' if user.is_active else 'success' }} btn-sm"
|
||||||
onclick="toggleUserStatus({{ user.id }}, '{{ 'warning' if user.is_active else 'success' }}')"
|
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>
|
<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>
|
</button>
|
||||||
{% if user.id != current_user.id %}
|
{% if user.id != current_user.id %}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-outline-danger btn-sm"
|
class="btn btn-outline-danger btn-sm"
|
||||||
onclick="deleteUser({{ user.id }})"
|
onclick="deleteUser({{ user.id }})"
|
||||||
title="Delete User">
|
title="Delete User Permanently"
|
||||||
|
data-bs-toggle="tooltip">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
|
<span class="d-none d-md-inline ms-1">Delete</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -164,6 +170,14 @@
|
|||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<script>
|
<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) {
|
function toggleUserStatus(userId, status) {
|
||||||
const button = event.target.closest('button');
|
const button = event.target.closest('button');
|
||||||
const isActive = status === 'warning';
|
const isActive = status === 'warning';
|
||||||
|
|||||||
Reference in New Issue
Block a user