248 lines
13 KiB
HTML
248 lines
13 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{% trans %}User Management{% endtrans %} - Edu-Space Admin{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid pt-5 mt-4">
|
|
<!-- Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1">
|
|
<i class="bi bi-people text-primary me-2"></i>{% trans %}User Management{% endtrans %}
|
|
</h1>
|
|
<p class="text-muted small mb-0">{% trans %}Manage user accounts, assign roles, and control active status.{% endtrans %}</p>
|
|
</div>
|
|
<div>
|
|
<a href="{{ url_for('admin.user_add') }}" class="btn btn-primary">
|
|
<i class="bi bi-person-plus me-1"></i>{% trans %}New User{% endtrans %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters & Search -->
|
|
<div class="card border-0 shadow-sm mb-4">
|
|
<div class="card-body p-3">
|
|
<form method="GET" class="row g-2 align-items-center">
|
|
<div class="col-md-5">
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text bg-body-tertiary border-end-0">
|
|
<i class="bi bi-search text-muted"></i>
|
|
</span>
|
|
<input type="text" name="search" class="form-control border-start-0"
|
|
placeholder="{% trans %}Search by name or email...{% endtrans %}"
|
|
value="{{ search }}">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select name="role" class="form-select form-select-sm">
|
|
<option value="">{% trans %}All Roles{% endtrans %}</option>
|
|
{% for r in roles %}
|
|
<option value="{{ r.id }}" {% if role_filter == (r.id|string) %}selected{% endif %}>{{ r.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<select name="status" class="form-select form-select-sm">
|
|
<option value="">{% trans %}All Statuses{% endtrans %}</option>
|
|
<option value="active" {% if status_filter == 'active' %}selected{% endif %}>{% trans %}Active{% endtrans %}</option>
|
|
<option value="inactive" {% if status_filter == 'inactive' %}selected{% endif %}>{% trans %}Inactive{% endtrans %}</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-2 d-flex gap-2">
|
|
<button type="submit" class="btn btn-primary btn-sm w-100">
|
|
<i class="bi bi-filter me-1"></i>{% trans %}Filter{% endtrans %}
|
|
</button>
|
|
{% if search or role_filter or status_filter %}
|
|
<a href="{{ url_for('admin.users_list') }}" class="btn btn-outline-secondary btn-sm" title="{% trans %}Clear filters{% endtrans %}">
|
|
<i class="bi bi-x-lg"></i>
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Users Table -->
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="bg-body-tertiary">
|
|
<tr>
|
|
<th class="ps-4">{% trans %}User{% endtrans %}</th>
|
|
<th>{% trans %}Email{% endtrans %}</th>
|
|
<th>{% trans %}Role / Group{% endtrans %}</th>
|
|
<th>{% trans %}Status{% endtrans %}</th>
|
|
<th>{% trans %}Last Login{% endtrans %}</th>
|
|
<th class="text-end pe-4">{% trans %}Actions{% endtrans %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for u in users %}
|
|
<tr>
|
|
<td class="ps-4">
|
|
<div class="d-flex align-items-center">
|
|
<div class="user-avatar-small me-2">
|
|
{{ (u.first_name[0] if u.first_name else 'U')|upper }}
|
|
</div>
|
|
<div>
|
|
<div class="fw-bold">{{ u.name }}</div>
|
|
<small class="text-muted">ID: #{{ u.id }}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="font-monospace small">{{ u.email }}</span>
|
|
</td>
|
|
<td>
|
|
{% if u.role_obj %}
|
|
<span class="badge badge-brand">
|
|
<i class="bi bi-shield-check me-1"></i>{{ u.role_obj.name }}
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">
|
|
{{ u.role or _('No Role') }}
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if u.is_active %}
|
|
<span class="badge bg-success-subtle text-success border border-success-subtle px-2 py-1">
|
|
<i class="bi bi-check-circle me-1"></i>{% trans %}Active{% endtrans %}
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-danger-subtle text-danger border border-danger-subtle px-2 py-1">
|
|
<i class="bi bi-dash-circle me-1"></i>{% trans %}Inactive{% endtrans %}
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<small class="text-muted">
|
|
{% if u.last_login %}
|
|
{{ format_datetime(u.last_login, format='short') }}
|
|
{% else %}
|
|
{% trans %}Never{% endtrans %}
|
|
{% endif %}
|
|
</small>
|
|
</td>
|
|
<td class="text-end pe-4">
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="{{ url_for('admin.user_edit', id=u.id) }}" class="btn btn-outline-primary" title="{% trans %}Edit User{% endtrans %}">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
{% if u.id != current_user.id %}
|
|
<form action="{{ url_for('admin.user_toggle', id=u.id) }}" method="POST" class="d-inline">
|
|
<button type="submit" class="btn {% if u.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}"
|
|
title="{% if u.is_active %}{% trans %}Deactivate User{% endtrans %}{% else %}{% trans %}Activate User{% endtrans %}{% endif %}">
|
|
<i class="bi {% if u.is_active %}bi-person-x{% else %}bi-person-check{% endif %}"></i>
|
|
</button>
|
|
</form>
|
|
<button type="button" class="btn btn-outline-danger btn-delete-user"
|
|
data-user-id="{{ u.id }}"
|
|
data-user-name="{{ u.name }}"
|
|
data-user-email="{{ u.email }}"
|
|
data-action-url="{{ url_for('admin.user_delete', id=u.id) }}"
|
|
title="{% trans %}Eliminar Usuario{% endtrans %}">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="6" class="text-center py-5 text-muted">
|
|
<i class="bi bi-people fs-1 d-block mb-2 text-secondary"></i>
|
|
{% trans %}No users found matching the criteria.{% endtrans %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if pagination.pages > 1 %}
|
|
<div class="card-footer bg-body-tertiary d-flex justify-content-between align-items-center py-2 px-4">
|
|
<small class="text-muted">
|
|
{% trans %}Showing page{% endtrans %} {{ pagination.page }} {% trans %}of{% endtrans %} {{ pagination.pages }} ({{ pagination.total }} {% trans %}total{% endtrans %})
|
|
</small>
|
|
<ul class="pagination pagination-sm mb-0">
|
|
{% if pagination.has_prev %}
|
|
<li class="page-item"><a class="page-link" href="{{ url_for('admin.users_list', page=pagination.prev_num, search=search, role=role_filter, status=status_filter) }}">«</a></li>
|
|
{% endif %}
|
|
{% for p in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
|
|
{% if p %}
|
|
<li class="page-item {% if p == pagination.page %}active{% endif %}">
|
|
<a class="page-link" href="{{ url_for('admin.users_list', page=p, search=search, role=role_filter, status=status_filter) }}">{{ p }}</a>
|
|
</li>
|
|
{% else %}
|
|
<li class="page-item disabled"><span class="page-link">…</span></li>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% if pagination.has_next %}
|
|
<li class="page-item"><a class="page-link" href="{{ url_for('admin.users_list', page=pagination.next_num, search=search, role=role_filter, status=status_filter) }}">»</a></li>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Global de Eliminación de Usuario (fuera de la tabla) -->
|
|
<div class="modal fade" id="deleteUserModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow-lg" style="border-radius: 14px;">
|
|
<div class="modal-header border-0 pb-0">
|
|
<div class="d-flex align-items-center">
|
|
<div class="badge bg-danger-subtle text-danger p-2 rounded-circle me-2">
|
|
<i class="bi bi-person-x-fill fs-5"></i>
|
|
</div>
|
|
<h5 class="modal-title fw-bold text-danger mb-0">{% trans %}Confirmar Eliminación de Usuario{% endtrans %}</h5>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body text-start py-3">
|
|
<p class="mb-2">{% trans %}¿Está seguro de que desea eliminar permanentemente al siguiente usuario?{% endtrans %}</p>
|
|
<div class="p-2 rounded bg-body-tertiary mb-2">
|
|
<div class="fw-bold text-primary" id="deleteUserName"></div>
|
|
<div class="small text-muted font-monospace" id="deleteUserEmail"></div>
|
|
</div>
|
|
<p class="text-muted small mb-0"><i class="bi bi-exclamation-triangle me-1 text-warning"></i>{% trans %}Esta acción desvinculará sus comisiones y reservas asociadas. No se puede deshacer.{% endtrans %}</p>
|
|
</div>
|
|
<div class="modal-footer border-0 pt-0">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">{% trans %}Cancelar{% endtrans %}</button>
|
|
<form id="deleteUserForm" method="POST" class="d-inline">
|
|
<button type="submit" class="btn btn-danger px-3">
|
|
<i class="bi bi-trash me-1"></i>{% trans %}Eliminar Usuario{% endtrans %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
var deleteModalEl = document.getElementById('deleteUserModal');
|
|
var deleteModal = deleteModalEl ? new bootstrap.Modal(deleteModalEl) : null;
|
|
var nameEl = document.getElementById('deleteUserName');
|
|
var emailEl = document.getElementById('deleteUserEmail');
|
|
var formEl = document.getElementById('deleteUserForm');
|
|
|
|
document.querySelectorAll('.btn-delete-user').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var name = this.getAttribute('data-user-name');
|
|
var email = this.getAttribute('data-user-email');
|
|
var actionUrl = this.getAttribute('data-action-url');
|
|
if (nameEl) nameEl.textContent = name;
|
|
if (emailEl) emailEl.textContent = email;
|
|
if (formEl) formEl.action = actionUrl;
|
|
if (deleteModal) deleteModal.show();
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|