🎯 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:
Gan, Jimmy
2025-07-11 13:34:56 +08:00
parent 2027c443d3
commit fefe9b10c0
3 changed files with 309 additions and 6 deletions
+16 -2
View File
@@ -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:
+276 -1
View File
@@ -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 %}
+17 -3
View File
@@ -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';