253 lines
9.4 KiB
HTML
253 lines
9.4 KiB
HTML
{% extends "admin/base.html" %}
|
|
|
|
{% block title %}Users - Admin Panel{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="card border-0 shadow-sm mb-4">
|
|
<div class="card-body">
|
|
<form method="get" class="row g-3 align-items-end">
|
|
<div class="col-md-8">
|
|
<label for="search" class="form-label">Search Users</label>
|
|
<input type="text" id="search" name="search" placeholder="Search by username or email..."
|
|
value="{{ request.args.get('search', '') }}" class="form-control">
|
|
</div>
|
|
<div class="col-md-4">
|
|
<button type="submit" class="btn btn-primary w-100">
|
|
<i class="fas fa-search me-2"></i>Search
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<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-users me-2 text-primary"></i>
|
|
User Management
|
|
</h5>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<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">Username</th>
|
|
<th class="border-0">Email</th>
|
|
<th class="border-0">Status</th>
|
|
<th class="border-0">Role</th>
|
|
<th class="border-0">Last Login</th>
|
|
<th class="border-0">Created</th>
|
|
<th class="border-0 text-center">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for user in users.items %}
|
|
<tr>
|
|
<td>
|
|
<span class="badge bg-secondary">#{{ user.id }}</span>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="avatar-sm bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
|
|
<i class="fas fa-user text-primary"></i>
|
|
</div>
|
|
<span class="fw-medium">{{ user.username }}</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="text-muted">{{ user.email }}</span>
|
|
</td>
|
|
<td>
|
|
<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>
|
|
</td>
|
|
<td>
|
|
<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>
|
|
</td>
|
|
<td>
|
|
<small class="text-muted">
|
|
{{ user.last_login.strftime('%m/%d/%Y %H:%M') if user.last_login else 'Never' }}
|
|
</small>
|
|
</td>
|
|
<td>
|
|
<small class="text-muted">
|
|
{{ user.created_at.strftime('%m/%d/%Y') }}
|
|
</small>
|
|
</td>
|
|
<td class="text-center">
|
|
<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">
|
|
<i class="fas fa-eye"></i>
|
|
</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">
|
|
<i class="fas fa-{{ 'ban' if user.is_active else 'check' }}"></i>
|
|
</button>
|
|
{% if user.id != current_user.id %}
|
|
<button type="button"
|
|
class="btn btn-outline-danger btn-sm"
|
|
onclick="deleteUser({{ user.id }})"
|
|
title="Delete User">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if users.pages > 1 %}
|
|
<div class="d-flex justify-content-between align-items-center mt-4">
|
|
<div class="text-muted">
|
|
Showing {{ users.items|length }} of {{ users.total }} users
|
|
</div>
|
|
<nav aria-label="User pagination">
|
|
<ul class="pagination pagination-sm mb-0">
|
|
{% if users.has_prev %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('admin.users', page=users.prev_num, search=request.args.get('search', '')) }}">
|
|
<i class="fas fa-chevron-left"></i>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% for page_num in users.iter_pages() %}
|
|
{% if page_num %}
|
|
{% if page_num != users.page %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('admin.users', page=page_num, search=request.args.get('search', '')) }}">
|
|
{{ page_num }}
|
|
</a>
|
|
</li>
|
|
{% else %}
|
|
<li class="page-item active">
|
|
<span class="page-link">{{ page_num }}</span>
|
|
</li>
|
|
{% endif %}
|
|
{% else %}
|
|
<li class="page-item disabled">
|
|
<span class="page-link">...</span>
|
|
</li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
|
|
{% if users.has_next %}
|
|
<li class="page-item">
|
|
<a class="page-link" href="{{ url_for('admin.users', page=users.next_num, search=request.args.get('search', '')) }}">
|
|
<i class="fas fa-chevron-right"></i>
|
|
</a>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
</nav>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
{{ super() }}
|
|
<script>
|
|
function toggleUserStatus(userId, status) {
|
|
const button = event.target.closest('button');
|
|
const isActive = status === 'warning';
|
|
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');
|
|
});
|
|
}
|
|
|
|
function deleteUser(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: userId })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.message) {
|
|
location.reload();
|
|
} 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>
|
|
.avatar-sm {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.table th {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
font-size: 0.875rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.table td {
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.btn-group .btn {
|
|
margin-right: 2px;
|
|
}
|
|
|
|
.btn-group .btn:last-child {
|
|
margin-right: 0;
|
|
}
|
|
</style>
|
|
{% endblock %} |