407 lines
22 KiB
HTML
407 lines
22 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Gestión de Usuarios — Edu-Space Admin{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid pt-5 mt-4">
|
|
<!-- Header -->
|
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3 mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1 fw-bold">
|
|
<i class="bi bi-people text-primary me-2"></i>Gestión de Usuarios
|
|
</h1>
|
|
<p class="text-muted small mb-0">
|
|
Administración de cuentas, identidad y documentación, datos de contacto y asignación de roles institucionales (Bedelía, Docentes, Alumnos).
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<a href="/admin/user_add" class="btn btn-primary shadow-sm">
|
|
<i class="bi bi-person-plus me-1"></i>Nuevo Usuario
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Feedback Alerts -->
|
|
{% if msg == 'saved' %}
|
|
<div class="alert alert-success alert-dismissible fade show shadow-sm mb-4" role="alert">
|
|
<i class="bi bi-check-circle-fill me-2"></i>Los datos del usuario han sido guardados exitosamente.
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
{% elif msg == 'password_updated' %}
|
|
<div class="alert alert-success alert-dismissible fade show shadow-sm mb-4" role="alert">
|
|
<i class="bi bi-shield-check me-2"></i>La contraseña del usuario ha sido restablecida exitosamente.
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
{% elif error %}
|
|
<div class="alert alert-danger alert-dismissible fade show shadow-sm mb-4" role="alert">
|
|
<i class="bi bi-exclamation-triangle-fill me-2"></i>{{ error }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- 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="Buscar por nombre, apellido, email o nro de documento..."
|
|
value="{{ search }}">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<select name="role" class="form-select form-select-sm">
|
|
<option value="">Todos los Roles</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="">Todos los Estados</option>
|
|
<option value="active" {% if status_filter == 'active' %}selected{% endif %}>Solo Activos</option>
|
|
<option value="inactive" {% if status_filter == 'inactive' %}selected{% endif %}>Solo Inactivos</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>Filtrar
|
|
</button>
|
|
{% if search or role_filter or status_filter %}
|
|
<a href="/admin/users_list" class="btn btn-outline-secondary btn-sm" title="Limpiar filtros">
|
|
<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">Usuario / Identidad</th>
|
|
<th>Documento</th>
|
|
<th>Contacto y Domicilio</th>
|
|
<th>Rol Institucional</th>
|
|
<th>Estado</th>
|
|
<th>Último Acceso</th>
|
|
<th class="text-end pe-4">Acciones</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 rounded-circle bg-primary-subtle text-primary d-flex align-items-center justify-content-center fw-bold" style="width: 36px; height: 36px;">
|
|
{{ (u.first_name[0] if u.first_name else (u.name[0] if u.name else 'U'))|upper }}
|
|
</div>
|
|
<div>
|
|
<div class="fw-bold">{{ u.name }}</div>
|
|
<div class="text-muted small">
|
|
{% if u.first_name and u.last_name %}
|
|
<span class="opacity-75">{{ u.last_name }}, {{ u.first_name }}</span> ·
|
|
{% endif %}
|
|
<span class="font-monospace">#{{ u.id }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{% if u.document_number %}
|
|
<div>
|
|
<span class="badge {% if u.document_type in ['PAS', 'PASEXT'] %}bg-info-subtle text-info border border-info-subtle{% elif u.document_type == 'CUIL' %}bg-primary-subtle text-primary border border-primary-subtle{% else %}bg-secondary-subtle text-secondary border{% endif %} py-1 px-2 font-monospace">
|
|
{{ u.document_type or 'DNI' }}: {{ u.document_formatted or u.document_number }}
|
|
</span>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-muted small fst-italic"><i class="bi bi-dash"></i> Sin registrar</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div>
|
|
<i class="bi bi-building me-1 text-primary" title="Email Institucional"></i>
|
|
<span class="font-monospace small fw-semibold">{{ u.email }}</span>
|
|
</div>
|
|
{% if u.personal_email %}
|
|
<div class="text-muted small">
|
|
<i class="bi bi-envelope-at me-1 text-secondary" title="Email Personal"></i>
|
|
<span class="font-monospace" style="font-size: 0.8rem;">{{ u.personal_email }}</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if u.phone or u.address %}
|
|
<div class="text-muted small mt-1 d-flex flex-column gap-0">
|
|
{% if u.phone %}
|
|
<span><i class="bi bi-telephone me-1 text-secondary"></i>{{ u.phone }}</span>
|
|
{% endif %}
|
|
{% if u.address %}
|
|
<span class="text-truncate" style="max-width: 220px;" title="{{ u.address }}">
|
|
<i class="bi bi-geo-alt me-1 text-secondary"></i>{{ u.address }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</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 'Docente' }}
|
|
</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>Activo
|
|
</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>Inactivo
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<small class="text-muted">
|
|
{% if u.last_login %}
|
|
{{ u.last_login[:16] if u.last_login is string else u.last_login }}
|
|
{% else %}
|
|
Nunca
|
|
{% endif %}
|
|
</small>
|
|
</td>
|
|
<td class="text-end pe-4">
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="/admin/users/edit/{{ u.id }}" class="btn btn-outline-primary" title="Editar Ficha de Usuario">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<button type="button" class="btn btn-outline-warning btn-reset-password"
|
|
data-user-id="{{ u.id }}"
|
|
data-user-name="{{ u.name }}"
|
|
data-user-email="{{ u.email }}"
|
|
title="Restablecer Contraseña">
|
|
<i class="bi bi-key"></i>
|
|
</button>
|
|
{% if u.id != user.id %}
|
|
<a href="/auth/impersonate/{{ u.id }}" class="btn btn-outline-info" title="Impersonar usuario (Iniciar sesión como {{ u.name }})">
|
|
<i class="bi bi-incognito"></i>
|
|
</a>
|
|
<form action="/admin/user_toggle" method="POST" class="d-inline">
|
|
<input type="hidden" name="id" value="{{ u.id }}">
|
|
<button type="submit" class="btn {% if u.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}"
|
|
title="{% if u.is_active %}Desactivar Usuario{% else %}Activar Usuario{% 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="/admin/user_delete"
|
|
title="Eliminar Usuario">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="7" class="text-center py-5 text-muted">
|
|
<i class="bi bi-people fs-1 d-block mb-2 text-secondary"></i>
|
|
No se encontraron usuarios coincidentes con los criterios de búsqueda.
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
{% if pagination and pagination.pages > 1 %}
|
|
<div class="card-footer bg-body-tertiary border-0 py-2 d-flex justify-content-center">
|
|
<ul class="pagination pagination-sm mb-0">
|
|
{% if pagination.has_prev %}
|
|
<li class="page-item"><a class="page-link" href="/admin/users_list">«</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="/admin/users_list">{{ 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="/admin/users_list">»</a></li>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Global de Eliminación de Usuario -->
|
|
<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">Confirmar Eliminación de Usuario</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">¿Está seguro de que desea eliminar permanentemente al siguiente usuario?</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>Esta acción desvinculará sus comisiones y reservas asociadas. No se puede deshacer.</p>
|
|
</div>
|
|
<div class="modal-footer border-0 pt-0">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</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>Eliminar Usuario
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Restablecimiento Directo de Contraseña -->
|
|
<div class="modal fade" id="resetPasswordModal" 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-warning-subtle text-warning p-2 rounded-circle me-2">
|
|
<i class="bi bi-key-fill fs-5"></i>
|
|
</div>
|
|
<h5 class="modal-title fw-bold mb-0">Restablecer Contraseña</h5>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="resetPasswordForm" method="POST" action="/admin/users/password">
|
|
<input type="hidden" name="id" id="resetPasswordUserId">
|
|
<div class="modal-body text-start py-3">
|
|
<p class="mb-2 text-muted small">
|
|
Ingrese la nueva contraseña de acceso institucional para el siguiente usuario:
|
|
</p>
|
|
<div class="p-2 rounded bg-body-tertiary mb-3">
|
|
<div class="fw-bold text-primary" id="resetPasswordUserName"></div>
|
|
<div class="small text-muted font-monospace" id="resetPasswordUserEmail"></div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="modalNewPassword" class="form-label small fw-semibold">Nueva Contraseña <span class="text-danger">*</span></label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-body-tertiary"><i class="bi bi-lock"></i></span>
|
|
<input type="password" class="form-control" id="modalNewPassword" name="password" required minlength="6" placeholder="Mínimo 6 caracteres">
|
|
<button class="btn btn-outline-secondary" type="button" id="toggleModalPasswordBtn" title="Mostrar/Ocultar clave">
|
|
<i class="bi bi-eye" id="toggleModalPasswordIcon"></i>
|
|
</button>
|
|
</div>
|
|
<div class="form-text small">
|
|
La contraseña debe contener al menos 6 caracteres.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer border-0 pt-0">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
|
|
<button type="submit" class="btn btn-warning px-3 fw-semibold">
|
|
<i class="bi bi-check-circle me-1"></i>Guardar Contraseña
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</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');
|
|
var userId = this.getAttribute('data-user-id');
|
|
if (nameEl) nameEl.textContent = name;
|
|
if (emailEl) emailEl.textContent = email;
|
|
if (formEl) {
|
|
formEl.action = actionUrl;
|
|
var hiddenId = formEl.querySelector('input[name="id"]');
|
|
if (!hiddenId) {
|
|
hiddenId = document.createElement('input');
|
|
hiddenId.type = 'hidden';
|
|
hiddenId.name = 'id';
|
|
formEl.appendChild(hiddenId);
|
|
}
|
|
hiddenId.value = userId;
|
|
}
|
|
if (deleteModal) deleteModal.show();
|
|
});
|
|
});
|
|
|
|
// Reset Password Modal
|
|
var resetModalEl = document.getElementById('resetPasswordModal');
|
|
var resetModal = resetModalEl ? new bootstrap.Modal(resetModalEl) : null;
|
|
var resetUserIdInput = document.getElementById('resetPasswordUserId');
|
|
var resetUserName = document.getElementById('resetPasswordUserName');
|
|
var resetUserEmail = document.getElementById('resetPasswordUserEmail');
|
|
var resetPasswordForm = document.getElementById('resetPasswordForm');
|
|
var modalNewPassword = document.getElementById('modalNewPassword');
|
|
|
|
document.querySelectorAll('.btn-reset-password').forEach(function(btn) {
|
|
btn.addEventListener('click', function() {
|
|
var userId = this.getAttribute('data-user-id');
|
|
var name = this.getAttribute('data-user-name');
|
|
var email = this.getAttribute('data-user-email');
|
|
if (resetUserIdInput) resetUserIdInput.value = userId;
|
|
if (resetUserName) resetUserName.textContent = name;
|
|
if (resetUserEmail) resetUserEmail.textContent = email;
|
|
if (resetPasswordForm) resetPasswordForm.action = '/admin/users/' + userId + '/password';
|
|
if (modalNewPassword) modalNewPassword.value = '';
|
|
if (resetModal) resetModal.show();
|
|
});
|
|
});
|
|
|
|
var toggleModalPasswordBtn = document.getElementById('toggleModalPasswordBtn');
|
|
var toggleModalPasswordIcon = document.getElementById('toggleModalPasswordIcon');
|
|
if (toggleModalPasswordBtn && modalNewPassword && toggleModalPasswordIcon) {
|
|
toggleModalPasswordBtn.addEventListener('click', function() {
|
|
var isPass = modalNewPassword.getAttribute('type') === 'password';
|
|
modalNewPassword.setAttribute('type', isPass ? 'text' : 'password');
|
|
toggleModalPasswordIcon.className = isPass ? 'bi bi-eye-slash' : 'bi bi-eye';
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|