feat(i18n & ux): auditoria, traduccion completa, correcciones ux y gestion de idioma en perfil

This commit is contained in:
Carlos Tello
2026-09-02 23:45:13 -03:00
parent 3d4e9992f8
commit d1d4be0428
31 changed files with 2356 additions and 488 deletions
+26
View File
@@ -0,0 +1,26 @@
{% extends "base.html" %}
{% block title %}Acceso Denegado (403) - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container pt-5 mt-5 text-center">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-0 shadow-sm p-5">
<div class="text-danger mb-3" style="font-size: 4rem;">
<i class="bi bi-shield-x"></i>
</div>
<h1 class="h2 mb-2">403 - Acceso Denegado</h1>
<p class="text-muted mb-4">
No tienes los permisos necesarios para acceder a este recurso. Esta sección está restringida exclusivamente a administradores.
</p>
<div>
<a href="{{ url_for('main.dashboard') }}" class="btn btn-primary">
<i class="bi bi-house me-1"></i>Ir al Dashboard
</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+62
View File
@@ -0,0 +1,62 @@
{% extends "base.html" %}
{% block title %}Cambiar Contraseña - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container pt-5 mt-4">
<div class="row justify-content-center">
<div class="col-lg-6 col-md-8">
<!-- Header -->
<div class="d-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0">
<i class="bi bi-shield-lock text-primary me-2"></i>Cambiar Contraseña
</h1>
<a href="{{ url_for('auth.profile') }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>Volver al Perfil
</a>
</div>
<!-- Form Card -->
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="POST" action="{{ url_for('auth.change_password') }}">
<div class="mb-3">
<label for="current_password" class="form-label">Contraseña Actual</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
<input type="password" class="form-control" id="current_password" name="current_password" required placeholder="Ingresa tu contraseña actual">
</div>
</div>
<div class="mb-3">
<label for="new_password" class="form-label">Nueva Contraseña</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="password" class="form-control" id="new_password" name="new_password" required minlength="6" placeholder="Mínimo 6 caracteres">
</div>
<div class="form-text">Debe tener al menos 6 caracteres.</div>
</div>
<div class="mb-4">
<label for="confirm_password" class="form-label">Confirmar Nueva Contraseña</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-check2-circle"></i></span>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required minlength="6" placeholder="Repite la nueva contraseña">
</div>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i>Actualizar Contraseña
</button>
<a href="{{ url_for('auth.profile') }}" class="btn btn-link text-muted">
Cancelar
</a>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+34 -20
View File
@@ -1,42 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ session.language or request.accept_languages.best_match(['es', 'en']) or 'es' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Edu-Space Admin</title>
<title>{% trans %}Login - Edu-Space Admin{% endtrans %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: system-ui, -apple-system, sans-serif;
}
.login-card {
background: rgba(255, 255, 255, 0.95);
background: rgba(30, 41, 59, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.5);
max-width: 450px;
width: 100%;
color: #f8fafc;
}
.logo-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
border-radius: 20px 20px 0 0;
padding: 2rem;
text-align: center;
color: white;
position: relative;
}
.lang-switch {
position: absolute;
top: 1rem;
right: 1rem;
}
</style>
</head>
<body>
<div class="login-card">
<div class="logo-section">
<div class="lang-switch">
{% include 'partials/language_switcher.html' %}
</div>
<i class="bi bi-building" style="font-size: 3rem;"></i>
<h3 class="mt-3">Edu-Space Admin</h3>
<p class="mb-0">Classroom Management System</p>
<h3 class="mt-3 mb-1 fw-bold">Edu-Space Admin</h3>
<p class="mb-0 text-white-50 small">{% trans %}Classroom Management System{% endtrans %}</p>
</div>
<div class="p-4">
@@ -55,10 +67,10 @@
{{ form.hidden_tag() }}
<div class="mb-3">
<label for="email" class="form-label">{{ form.email.label.text }}</label>
<label for="email" class="form-label small text-secondary">{{ form.email.label.text }}</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
{{ form.email(class="form-control", placeholder="Enter your email") }}
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-envelope"></i></span>
<input type="email" class="form-control bg-dark border-secondary text-white" id="email" name="email" value="{{ form.email.data or '' }}" required placeholder="{% trans %}Enter your email{% endtrans %}">
</div>
{% for error in form.email.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
@@ -66,10 +78,10 @@
</div>
<div class="mb-3">
<label for="password" class="form-label">{{ form.password.label.text }}</label>
<label for="password" class="form-label small text-secondary">{{ form.password.label.text }}</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
{{ form.password(class="form-control", placeholder="Enter your password") }}
<span class="input-group-text bg-dark border-secondary text-secondary"><i class="bi bi-lock"></i></span>
<input type="password" class="form-control bg-dark border-secondary text-white" id="password" name="password" required placeholder="{% trans %}Enter your password{% endtrans %}">
</div>
{% for error in form.password.errors %}
<div class="text-danger small mt-1">{{ error }}</div>
@@ -78,18 +90,20 @@
<div class="mb-3 form-check">
{{ form.remember_me(class="form-check-input") }}
{{ form.remember_me.label(class="form-check-label") }}
<label class="form-check-label text-secondary small" for="remember_me">{{ form.remember_me.label.text }}</label>
</div>
<div class="d-grid">
{{ form.submit(class="btn btn-primary btn-lg") }}
<button type="submit" class="btn btn-primary btn-lg fw-semibold">
<i class="bi bi-box-arrow-in-right me-1"></i> {{ form.submit.label.text }}
</button>
</div>
</form>
<div class="text-center mt-3">
<small class="text-muted">
<i class="bi bi-shield-lock"></i>
Secure Login • All access is logged
<div class="text-center mt-4">
<small class="text-secondary">
<i class="bi bi-shield-lock me-1"></i>
{% trans %}Secure Login • All access is logged{% endtrans %}
</small>
</div>
</div>
+161
View File
@@ -0,0 +1,161 @@
{% extends "base.html" %}
{% block title %}{% trans %}User Profile{% endtrans %} - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container pt-5 mt-4">
<div class="row justify-content-center">
<div class="col-lg-8">
<!-- Header -->
<div class="d-flex align-items-center justify-content-between mb-4">
<h1 class="h3 mb-0">
<i class="bi bi-person-badge text-primary me-2"></i>{% trans %}My Profile{% endtrans %}
</h1>
<a href="{{ url_for('main.dashboard') }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left me-1"></i>{% trans %}Dashboard{% endtrans %}
</a>
</div>
<!-- Profile Card -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body p-4">
<div class="d-flex align-items-center mb-4">
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 72px; height: 72px; font-size: 2rem;">
<i class="bi bi-person"></i>
</div>
<div>
<h4 class="mb-1">{{ current_user.name }}</h4>
<div class="d-flex align-items-center gap-2">
<span class="badge bg-primary">{{ current_user.role }}</span>
{% if current_user.is_active %}
<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>{% trans %}Active{% endtrans %}</span>
{% else %}
<span class="badge bg-danger"><i class="bi bi-x-circle me-1"></i>{% trans %}Inactive{% endtrans %}</span>
{% endif %}
</div>
</div>
</div>
<hr class="my-4">
<!-- User Details Grid -->
<div class="row g-3">
<div class="col-sm-6">
<label class="form-label text-muted small mb-1">{% trans %}Email{% endtrans %}</label>
<div class="fw-semibold">{{ current_user.email }}</div>
</div>
<div class="col-sm-6">
<label class="form-label text-muted small mb-1">{% trans %}Full Name{% endtrans %}</label>
<div class="fw-semibold">{{ current_user.name }}</div>
</div>
<div class="col-sm-6">
<label class="form-label text-muted small mb-1">{% trans %}Access Role{% endtrans %}</label>
<div class="fw-semibold">{{ current_user.role }}</div>
</div>
<div class="col-sm-6">
<label class="form-label text-muted small mb-1">{% trans %}Last Login{% endtrans %}</label>
<div class="fw-semibold">
{% if current_user.last_login %}
{{ current_user.last_login.strftime('%Y-%m-%d %H:%M:%S') }}
{% else %}
<span class="text-muted">{% trans %}First login{% endtrans %}</span>
{% endif %}
</div>
</div>
<div class="col-sm-6">
<label class="form-label text-muted small mb-1">{% trans %}Registration Date{% endtrans %}</label>
<div class="fw-semibold">
{% if current_user.created_at %}
{{ current_user.created_at.strftime('%Y-%m-%d') }}
{% else %}
<span class="text-muted">-</span>
{% endif %}
</div>
</div>
</div>
<hr class="my-4">
<!-- Language Preference Section -->
<form method="POST" action="{{ url_for('auth.profile') }}">
<label class="form-label fw-bold mb-2">
<i class="bi bi-translate text-primary me-2"></i>{% trans %}Language Preference{% endtrans %}
</label>
<p class="text-muted small mb-3">
{% trans %}If set, this language overrides the browser configuration for all your sessions.{% endtrans %}
</p>
<div class="row g-2 align-items-center">
<div class="col-sm-8">
<select class="form-select" name="preferred_language">
<option value="" {% if not current_user.preferred_language %}selected{% endif %}>
{% trans %}Auto-detect from browser (Default){% endtrans %}
</option>
<option value="es" {% if current_user.preferred_language == 'es' %}selected{% endif %}>
Español
</option>
<option value="en" {% if current_user.preferred_language == 'en' %}selected{% endif %}>
English
</option>
</select>
</div>
<div class="col-sm-4">
<button type="submit" class="btn btn-outline-primary w-100">
<i class="bi bi-check2 me-1"></i>{% trans %}Save Preference{% endtrans %}
</button>
</div>
</div>
</form>
</div>
<div class="card-footer bg-body-tertiary px-4 py-3 d-flex justify-content-between align-items-center">
<span class="text-muted small">{% trans %}Account Security{% endtrans %}</span>
<a href="{{ url_for('auth.change_password') }}" class="btn btn-primary btn-sm">
<i class="bi bi-key me-1"></i>{% trans %}Change Password{% endtrans %}
</a>
</div>
</div>
<!-- User Reservations Overview (if any) -->
{% if current_user.reservations %}
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-body-tertiary">
<h5 class="card-title mb-0 h6">
<i class="bi bi-calendar-event me-2"></i>{% trans %}My Recent Reservations{% endtrans %}
</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>{% trans %}Classroom{% endtrans %}</th>
<th>{% trans %}Purpose{% endtrans %}</th>
<th>{% trans %}Date & Time{% endtrans %}</th>
<th>{% trans %}Status{% endtrans %}</th>
</tr>
</thead>
<tbody>
{% for res in current_user.reservations[:5] %}
<tr>
<td>
<span class="badge bg-secondary">{{ res.classroom.code_display if res.classroom else 'N/A' }}</span>
</td>
<td>{{ res.purpose }}</td>
<td>{{ res.start_time.strftime('%Y-%m-%d %H:%M') }}</td>
<td>
<span class="badge bg-{{ 'success' if res.status == 'CONFIRMED' else 'warning' if res.status == 'PENDING' else 'secondary' }}">
{{ res.status }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}