Initial commit: Clean Mold Cost Calculator application
- Flask-based mold cost calculation application - PostgreSQL database with SQLAlchemy ORM - Docker/Podman containerization - Comprehensive documentation in docs/ - Clean, organized codebase without obsolete files - Production-ready deployment configuration - Enhanced pytest configuration with coverage reporting - Master/Development branch structure
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
{% 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 %}
|
||||
Reference in New Issue
Block a user