diff --git a/CHANGELOG_SESSION.md b/CHANGELOG_SESSION.md index 4c76f46..a59635d 100644 --- a/CHANGELOG_SESSION.md +++ b/CHANGELOG_SESSION.md @@ -488,6 +488,43 @@ * **Pruebas y Verificación:** * Incorporación de tests en `tests/test_view_switcher.py`, validando renderizado de selectores y contenedores en ambas rutas (20/20 pruebas aprobadas). +--- + +## 29. Reorganización de Navegación según Estándares de la Industria & Dashboards Personalizados por Rol (2026-09-05) +* **Reorganización Estándar de la Arquitectura de Información (IA) en Menú de Navegación (`app/templates/base.html`):** + * **Eliminación de Redundancias y Duplicidades:** Se erradicaron los enlaces repetidos entre Aulas, Cronograma y Gestión (como Comisiones, Edificios y Reservas que aparecían en múltiples desplegables simultáneos), alineando el sistema a los estándares de experiencia de usuario (UX) de plataformas de gestión educativa y campus ERP de clase mundial (Canvas, Blackboard, Workday Student). + * **Estructuración Basada en Roles (RBAC Navigation):** + * **Administrador (`Admin`):** Menú exhaustivo y jerárquico organizado en: `Panel General`, `Espacios & Sedes` (Aulas presenciales, virtuales, edificios y métricas), `Cronograma & Reservas` (Calendario, cartelera, reservas, comisiones y optimizador IA), `Gestión Académica` (Carreras, materias, ciclo lectivo) y `Administración` (Usuarios, roles RBAC, hitos, sincronización Sheets y registro de auditoría). + * **Bedelía (`Bedelia`):** Menú orientado a la operación física y diaria del campus: `Panel Bedelía`, `Espacios & Aulas`, `Operaciones & Reservas` (Cartelera en vivo, asignación de aulas, gestión de reservas) y `Académica & Cursadas` (Ciclo lectivo, materias, tipificaciones de hitos e importación Sheets). + * **Profesor / Docente (`Docente`):** Menú simplificado y enfocado en la labor de enseñanza: `Panel Docente`, `Mis Clases & Reservas` (Mis reservas de aulas, solicitud de espacio, mis comisiones) y `Campus & Horarios` (Cartelera diaria, calendario general de aulas, aulas virtuales y catálogo presencial). + * **Alumno / Estudiante (`Alumno`):** Navegación directa y sin sobrecarga cognitiva: `Mi Portal`, `Cartelera de Hoy` (¿Dónde curso hoy? Con piso y aula), `Cronograma de Clases` (Calendario semanal), `Aulas Virtuales` (Acceso directo a videollamadas) y `Espacios del Campus`. + * **Identidad de Rol en Navbar:** Inclusión de insignia institucional con icono y color de rol en el menú de usuario (`role-pill`), permitiendo rápida identificación del perfil activo (`badge-admin`, `badge-bedelia`, `badge-docente`, `badge-alumno`). +* **Personalización Integral del Dashboard por Perfil Institucional (`app/routes/main.py` y `app/templates/dashboard.html`):** + * **Banner de Bienvenida & Selector de Perspectiva:** Saludo personalizado con badge del rol y selector reactivo (`?view_as=admin|bedelia|docente|alumno`) para administradores y evaluadores, facilitando la visualización inmediata de los menús y paneles de cada rol. + * **Dashboard Administrador:** + * 4 KPIs estratégicos: Aulas Totales, Oferta de Materias, Usuarios Activos y Reservas de Hoy. + * Módulos de Gestión Visual (Tarjetas de lanzamiento rápido a Espacios, Cronograma, Optimizador IA y Auditoría). + * Gráfico mensual de tendencias de reservas (`Chart.js`) y tabla de los últimos eventos de la bitácora de auditoría. + * **Dashboard Bedelía:** + * 4 KPIs operativos: Aulas Presenciales Operativas, Salas Virtuales, Clases del Día y Solicitudes Pendientes de Aprobación. + * Banner interactivo de advertencia de solicitudes pendientes con acceso de un clic para revisar y aprobar. + * Atajos operativos para cartelera en vivo, registro de reservas, sincronización Sheets y métricas de uso. + * **Dashboard Profesor / Docente:** + * 4 KPIs de cursada: Mis Clases Hoy, Mis Reservas Activas, Salas Virtuales y Acceso Directo para Solicitar Aula. + * Sección destacada "Mis Clases Programadas para Hoy" con horario, materia, comisión, ubicación física (edificio/piso/aula) o botón directo "Ingresar a Reunión" en clases virtuales. + * Accesos directos a comisiones a cargo y cartelera. + * **Dashboard Alumno / Estudiante:** + * Brújula diaria "¿Dónde curso hoy?": Cartelera personalizada que indica claramente la materia, horario, edificio, piso y número de aula o enlace a videollamada para clases remotas. + * Sección de "Próximos Hitos Académicos & Exámenes" con fechas de parciales, finales y entregas. + * Menú visual del alumno para consulta de cronograma semanal y salas virtuales. +* **Módulo de Registro de Auditoría (`app/routes/admin.py` y `app/templates/admin/audit_logs.html`):** + * Creación del endpoint `@admin_bp.route('/audit-logs')` con filtrado por texto, módulo y acción (CREATE, UPDATE, DELETE, LOGIN). + * Interfaz administrativa de auditoría con paginación y badges de severidad. +* **Normalización de Roles en Modelo de Dominio (`app/models/user.py`):** + * Incorporación de métodos de consulta `get_institutional_role()`, `is_bedelia()`, `is_docente()`, `is_alumno()` y propiedad `role_badge_display`. +* **Pruebas Automatizadas & Calidad:** + * Creación de `tests/test_role_dashboard_and_menu.py` con 6 casos de prueba unitarios e integrales (26/26 pruebas aprobadas en la suite general). + diff --git a/README.md b/README.md index 2335009..dd38ced 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,39 @@ La plataforma implementa una arquitectura desacoplada de visualización (`app/st * **Preservación de Filtros:** Inyección automática del parámetro `display` en formularios y sincronización de URL sin recarga (`history.replaceState`). * **Arquitectura Extensible:** Permite registrar futuras vistas (kanban, calendario, compacta) mediante atributos `data-view` y `data-view-container` sin alterar la lógica de negocio. +### 🧭 10. Arquitectura de Información & Navegación Basada en Estándares de la Industria +La barra de navegación principal (`app/templates/base.html`) fue rediseñada integralmente bajo las mejores prácticas de experiencia de usuario (UX) y arquitectura de información (IA) de sistemas ERP universitarios de referencia internacional (Canvas, Blackboard, Workday Student): +* **Eliminación de Redundancias:** Supresión de destinos duplicados que anteriormente saturaban los menús desplegables. +* **Menús Estructurados por Rol Institucional:** + * **Administrador (`Admin`):** Menú de gobernanza integral dividido en `Panel General`, `Espacios & Sedes`, `Cronograma & Reservas`, `Gestión Académica` y `Administración & Seguridad` (con Auditoría, Usuarios y Roles RBAC). + * **Bedelía (`Bedelia`):** Menú operativo del campus dividido en `Panel Bedelía`, `Espacios & Aulas`, `Operaciones & Reservas` (Cartelera en tiempo real, registro de reservas) y `Académica & Cursadas`. + * **Docente / Profesor (`Docente`):** Menú simplificado y centrado en la enseñanza dividido en `Panel Docente`, `Mis Clases & Reservas` (Mis reservas, solicitar aula, comisiones) y `Campus & Horarios` (Cartelera, calendario y aulas virtuales). + * **Alumno / Estudiante (`Alumno`):** Navegación directa y accesible: `Mi Portal`, `Cartelera de Hoy` (¿Dónde curso hoy?), `Cronograma de Clases`, `Aulas Virtuales` y `Espacios del Campus`. +* **Identidad Visual del Usuario:** Badge institucional con gradiente de color e icono específico de rol (`badge-admin`, `badge-bedelia`, `badge-docente`, `badge-alumno`) integrado en el avatar de usuario. + +### 🎯 11. Dashboards Personalizados por Rol Institucional & Selector de Perspectiva +El panel de control (`app/routes/main.py` y `app/templates/dashboard.html`) adapta dinámicamente sus KPIs, accesos directos y widgets según la identidad del usuario logueado: +* **Dashboard Administrador:** + * KPIs estratégicos: Aulas totales, oferta de materias, usuarios registrados y reservas de hoy. + * Módulos visuales de lanzamiento rápido a las áreas clave del sistema. + * Gráfico analítico de evolución mensual de reservas (`Chart.js`) y tabla de eventos recientes de auditoría. +* **Dashboard Bedelía:** + * KPIs de infraestructura: Aulas físicas operativas, salas virtuales, clases del día y reservas pendientes. + * **Banner de Alerta Operativa:** Destaca solicitudes pendientes de aprobación con acceso de un clic para confirmarlas. + * Atajos para supervisión de cartelera en vivo, asignación de espacios, sincronización Google Sheets y ocupación. +* **Dashboard Profesor / Docente:** + * KPIs del docente: Clases del día, reservas activas, salas virtuales disponibles y botón destacado de "Solicitar Aula". + * **Widget "Mis Clases Programadas para Hoy":** Muestra horarios exactos, comisiones, aula física (Edificio/Piso/Aula) o botón directo "Ingresar a Reunión" para clases remotas. +* **Dashboard Alumno / Estudiante:** + * **Brújula Diaria "¿Dónde curso hoy?":** Tabla clara y legible con horario, materia, comisión, y ubicación exacta (Edificio · Piso · Aula) o botón de videoconferencia si la clase es virtual. + * **Próximos Hitos Académicos & Exámenes:** Calendario de parciales, finales y entregas programadas. +* **Selector de Perspectiva (Vista Previa de Roles):** + * Los administradores y evaluadores cuentan con un control selector en la cabecera del dashboard que permite conmutar la visualización entre los 4 roles (`?view_as=admin|bedelia|docente|alumno`) para auditar la experiencia de cada perfil institucional. + +### 🛡️ 12. Bitácora de Auditoría Institucional (`/admin/audit-logs`) +* Registro inmutable de eventos institucionales críticos (creación, edición, borrado de aulas, reservas y autenticación). +* Interfaz con filtrado multifactor (búsqueda por texto, módulo y tipo de acción), paginación y trazabilidad de dirección IP y usuario actuante. + --- ## 🛠️ Stack Tecnológico diff --git a/app/models/user.py b/app/models/user.py index cd71584..1750cfe 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -45,6 +45,43 @@ class User(UserMixin, db.Model): return True return str(self.role).upper() in ['ADMIN', 'ADMINISTRADOR'] + def get_institutional_role(self): + """Returns normalized institutional role: 'admin', 'bedelia', 'docente', 'alumno'""" + raw = '' + if self.role_obj and self.role_obj.name: + raw = self.role_obj.name.lower().strip() + elif self.role: + raw = str(self.role).lower().strip() + + if 'admin' in raw: + return 'admin' + if 'bedel' in raw: + return 'bedelia' + if 'docent' in raw or 'prof' in raw or 'teacher' in raw: + return 'docente' + if 'alumn' in raw or 'estud' in raw or 'student' in raw: + return 'alumno' + return 'docente' + + def is_bedelia(self): + return self.get_institutional_role() == 'bedelia' + + def is_docente(self): + return self.get_institutional_role() == 'docente' + + def is_alumno(self): + return self.get_institutional_role() == 'alumno' + + @property + def role_badge_display(self): + role_map = { + 'admin': {'label': 'Administrador', 'class': 'badge-admin', 'icon': 'bi-shield-check', 'color': 'primary'}, + 'bedelia': {'label': 'Bedelía', 'class': 'badge-bedelia', 'icon': 'bi-building-gear', 'color': 'warning'}, + 'docente': {'label': 'Profesor / Docente', 'class': 'badge-docente', 'icon': 'bi-person-video3', 'color': 'info'}, + 'alumno': {'label': 'Alumno / Estudiante', 'class': 'badge-alumno', 'icon': 'bi-mortarboard', 'color': 'success'} + } + return role_map.get(self.get_institutional_role(), {'label': self.role or 'Docente', 'class': 'bg-secondary', 'icon': 'bi-person', 'color': 'secondary'}) + def has_permission(self, module, min_level='read'): """Check if user has granular permission on a given module""" if self.is_admin(): @@ -57,6 +94,7 @@ class User(UserMixin, db.Model): """Check if user can access the Management / Admin section""" return ( self.is_admin() or + self.is_bedelia() or self.has_permission('users', 'read') or self.has_permission('milestone_types', 'read') or self.has_permission('import', 'read') or diff --git a/app/routes/admin.py b/app/routes/admin.py index 6b6d555..82fa356 100644 --- a/app/routes/admin.py +++ b/app/routes/admin.py @@ -5,6 +5,7 @@ from app import db from app.models.user import User from app.models.role import Role, Permission, SYSTEM_MODULES, ACCESS_LEVELS from app.models.milestone import MilestoneType, AcademicMilestone, BASE_MILESTONE_TYPES +from app.models.audit_log import AuditLog from app.utils.decorators import admin_required, permission_required from sqlalchemy import or_ import re @@ -1116,4 +1117,51 @@ def occupancy_metrics(): ) +# --------------------------------------------------------- +# AUDIT LOGS +# --------------------------------------------------------- + +@admin_bp.route('/audit-logs') +@admin_required +def audit_logs(): + """Vista de bitácora de auditoría del sistema""" + page = request.args.get('page', 1, type=int) + module_filter = request.args.get('module', '').strip() + action_filter = request.args.get('action', '').strip() + search = request.args.get('search', '').strip() + per_page = 20 + + query = AuditLog.query + + if module_filter: + query = query.filter(AuditLog.module == module_filter) + if action_filter: + query = query.filter(AuditLog.action == action_filter) + if search: + query = query.filter( + or_( + AuditLog.user_email.ilike(f'%{search}%'), + AuditLog.details.ilike(f'%{search}%'), + AuditLog.ip_address.ilike(f'%{search}%') + ) + ) + + pagination = query.order_by(AuditLog.created_at.desc()).paginate(page=page, per_page=per_page, error_out=False) + + # Módulos y acciones únicas para los selectores de filtro + modules = [r[0] for r in db.session.query(AuditLog.module).distinct().all() if r[0]] + actions = [r[0] for r in db.session.query(AuditLog.action).distinct().all() if r[0]] + + return render_template( + 'admin/audit_logs.html', + pagination=pagination, + audit_logs=pagination.items, + modules=modules, + actions=actions, + current_module=module_filter, + current_action=action_filter, + search=search + ) + + diff --git a/app/routes/main.py b/app/routes/main.py index 29bb092..be462e1 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -14,28 +14,40 @@ main_bp = Blueprint('main', __name__) @main_bp.route('/') @login_required def dashboard(): - """Main dashboard page""" -# Get statistics + """Role-personalized institutional dashboard page""" + from app.models.user import User + from app.models.milestone import AcademicMilestone + from app.models.audit_log import AuditLog + + actual_role = current_user.get_institutional_role() + view_as = request.args.get('view_as', '').lower().strip() + + # Allow admins and management roles to preview dashboard perspectives + if view_as in ['admin', 'bedelia', 'docente', 'alumno'] and (current_user.is_admin() or current_user.can_manage()): + role = view_as + else: + role = actual_role + + today = datetime.utcnow().date() + + # Common core statistics try: total_classrooms = Classroom.query.filter_by(is_active=True).count() - except: + except Exception: total_classrooms = 0 try: total_subjects = Subject.query.filter_by(is_active=True).count() - except: + except Exception: total_subjects = 0 - # Get today's reservations - today = datetime.utcnow().date() try: today_reservations = Reservation.query.filter( func.date(Reservation.start_time) == today ).count() - except: + except Exception: today_reservations = 0 - # Get this week's reservations week_start = today - timedelta(days=today.weekday()) week_end = week_start + timedelta(days=6) try: @@ -43,18 +55,9 @@ def dashboard(): func.date(Reservation.start_time) >= week_start, func.date(Reservation.start_time) <= week_end ).count() - except: + except Exception: week_reservations = 0 - # Get upcoming reservations (next 7 days) - try: - upcoming_reservations = Reservation.query.filter( - Reservation.start_time >= datetime.utcnow(), - Reservation.start_time <= datetime.utcnow() + timedelta(days=7) - ).limit(10).all() - except: - upcoming_reservations = [] - stats = { 'total_classrooms': total_classrooms, 'total_subjects': total_subjects, @@ -62,8 +65,180 @@ def dashboard(): 'week_reservations': week_reservations } - return render_template('dashboard.html', - stats=stats, + role_data = {} + + if role == 'admin': + # 1. Admin Institutional Cockpit + try: + total_users = User.query.filter_by(is_active=True).count() + except Exception: + total_users = 0 + + try: + pending_count = Reservation.query.filter( + Reservation.status == 'PENDING' + ).count() + except Exception: + pending_count = 0 + + try: + recent_audits = AuditLog.query.order_by(AuditLog.created_at.desc()).limit(6).all() + except Exception: + recent_audits = [] + + try: + upcoming_reservations = Reservation.query.filter( + Reservation.start_time >= datetime.utcnow(), + Reservation.start_time <= datetime.utcnow() + timedelta(days=7) + ).order_by(Reservation.start_time.asc()).limit(8).all() + except Exception: + upcoming_reservations = [] + + stats.update({ + 'total_users': total_users, + 'pending_reservations': pending_count + }) + role_data = { + 'recent_audits': recent_audits, + 'upcoming_reservations': upcoming_reservations + } + + elif role == 'bedelia': + # 2. Bedelía Operations & Facilities Control + try: + physical_classrooms = Classroom.query.filter_by(is_active=True).filter( + ~Classroom.room_number.ilike('%VIRTUAL%') & ~Classroom.building.ilike('%VIRTUAL%') + ).count() + virtual_classrooms = total_classrooms - physical_classrooms + if virtual_classrooms < 0: + virtual_classrooms = 0 + except Exception: + physical_classrooms = total_classrooms + virtual_classrooms = 0 + + try: + pending_reservations = Reservation.query.filter( + Reservation.status == 'PENDING' + ).order_by(Reservation.start_time.asc()).limit(10).all() + pending_count = len(pending_reservations) + except Exception: + pending_reservations = [] + pending_count = 0 + + try: + today_schedule_list = Reservation.query.filter( + func.date(Reservation.start_time) == today + ).order_by(Reservation.start_time.asc()).limit(12).all() + except Exception: + today_schedule_list = [] + + occupancy_rate = min(100, int((today_reservations / max(1, physical_classrooms * 3)) * 100)) if physical_classrooms > 0 else 0 + + stats.update({ + 'physical_classrooms': physical_classrooms, + 'virtual_classrooms': virtual_classrooms, + 'pending_count': pending_count, + 'occupancy_rate': occupancy_rate + }) + role_data = { + 'pending_reservations': pending_reservations, + 'today_schedule_list': today_schedule_list + } + + elif role == 'docente': + # 3. Docente (Profesor) Academic & Classroom Workflow + try: + my_today_classes = Reservation.query.filter( + Reservation.user_id == current_user.id, + func.date(Reservation.start_time) == today + ).order_by(Reservation.start_time.asc()).all() + except Exception: + my_today_classes = [] + + try: + my_upcoming_reservations = Reservation.query.filter( + Reservation.user_id == current_user.id, + Reservation.start_time >= datetime.utcnow() + ).order_by(Reservation.start_time.asc()).limit(8).all() + except Exception: + my_upcoming_reservations = [] + + try: + my_total_reservations = Reservation.query.filter( + Reservation.user_id == current_user.id + ).count() + except Exception: + my_total_reservations = 0 + + try: + virtual_classrooms = Classroom.query.filter_by(is_active=True).filter( + (Classroom.room_number.ilike('%VIRTUAL%')) | (Classroom.building.ilike('%VIRTUAL%')) + ).limit(4).all() + except Exception: + virtual_classrooms = [] + + stats.update({ + 'my_today_count': len(my_today_classes), + 'my_total_reservations': my_total_reservations, + 'virtual_rooms_count': len(virtual_classrooms) + }) + role_data = { + 'my_today_classes': my_today_classes, + 'my_upcoming_reservations': my_upcoming_reservations, + 'virtual_classrooms': virtual_classrooms + } + + elif role == 'alumno': + # 4. Alumno (Estudiante) Daily Guide & Timetable + try: + today_classes = Reservation.query.filter( + func.date(Reservation.start_time) == today, + Reservation.status == 'CONFIRMED' + ).order_by(Reservation.start_time.asc()).limit(12).all() + except Exception: + today_classes = [] + + try: + upcoming_milestones = AcademicMilestone.query.filter( + AcademicMilestone.date >= datetime.utcnow(), + AcademicMilestone.is_active == True + ).order_by(AcademicMilestone.date.asc()).limit(6).all() + except Exception: + upcoming_milestones = [] + + try: + virtual_rooms = Classroom.query.filter_by(is_active=True).filter( + (Classroom.room_number.ilike('%VIRTUAL%')) | (Classroom.building.ilike('%VIRTUAL%')) + ).limit(4).all() + except Exception: + virtual_rooms = [] + + stats.update({ + 'today_classes_count': len(today_classes), + 'milestones_count': len(upcoming_milestones) + }) + role_data = { + 'today_classes': today_classes, + 'upcoming_milestones': upcoming_milestones, + 'virtual_rooms': virtual_rooms + } + + # Backward compatibility for upcoming_reservations + upcoming_reservations = role_data.get('upcoming_reservations', []) + if not upcoming_reservations: + try: + upcoming_reservations = Reservation.query.filter( + Reservation.start_time >= datetime.utcnow(), + Reservation.start_time <= datetime.utcnow() + timedelta(days=7) + ).order_by(Reservation.start_time.asc()).limit(6).all() + except Exception: + upcoming_reservations = [] + + return render_template('dashboard.html', + role=role, + actual_role=actual_role, + stats=stats, + role_data=role_data, upcoming_reservations=upcoming_reservations) @main_bp.route('/api/dashboard-stats') diff --git a/app/static/css/style.css b/app/static/css/style.css index 7952e30..c987304 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -206,4 +206,99 @@ transform: translateY(-2px); box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.08) !important; border-color: var(--bs-primary) !important; +} + +/* Role Badges & Pills */ +.badge-admin { + background: linear-gradient(135deg, #7C3AED 0%, #B43E8E 100%) !important; + color: #FFFFFF !important; + border: 1px solid rgba(255, 255, 255, 0.25); +} + +.badge-bedelia { + background: linear-gradient(135deg, #D97706 0%, #B45309 100%) !important; + color: #FFFFFF !important; + border: 1px solid rgba(255, 255, 255, 0.25); +} + +.badge-docente { + background: linear-gradient(135deg, #0284C7 0%, #0369A1 100%) !important; + color: #FFFFFF !important; + border: 1px solid rgba(255, 255, 255, 0.25); +} + +.badge-alumno { + background: linear-gradient(135deg, #059669 0%, #047857 100%) !important; + color: #FFFFFF !important; + border: 1px solid rgba(255, 255, 255, 0.25); +} + +.role-pill { + font-size: 0.72rem; + font-weight: 600; + padding: 0.22rem 0.55rem; + border-radius: 9999px; + letter-spacing: 0.3px; + display: inline-flex; + align-items: center; + gap: 0.35rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12); +} + +/* Role Dashboard Hero & Cards */ +.dashboard-role-banner { + border-radius: 1rem; + position: relative; + overflow: hidden; + background: linear-gradient(135deg, rgba(180, 62, 142, 0.08) 0%, rgba(104, 44, 173, 0.12) 100%); + border: 1px solid rgba(180, 62, 142, 0.18); + padding: 1.5rem 1.75rem; +} + +[data-bs-theme="dark"] .dashboard-role-banner { + background: linear-gradient(135deg, rgba(180, 62, 142, 0.15) 0%, rgba(42, 27, 61, 0.5) 100%); + border-color: rgba(180, 62, 142, 0.25); +} + +.role-view-selector .btn { + font-size: 0.78rem; + font-weight: 600; + padding: 0.3rem 0.75rem; + border-radius: 9999px; + transition: all 0.2s ease; +} + +.role-view-selector .btn.active { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); +} + +.action-menu-card { + border-radius: 0.85rem; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + border: 1px solid rgba(0, 0, 0, 0.08); + text-decoration: none !important; +} + +.action-menu-card:hover { + transform: translateY(-3px); + box-shadow: 0 0.5rem 1.2rem rgba(0, 0, 0, 0.08) !important; + border-color: var(--brand-primary) !important; +} + +[data-bs-theme="dark"] .action-menu-card { + border-color: rgba(255, 255, 255, 0.08); +} + +[data-bs-theme="dark"] .action-menu-card:hover { + border-color: var(--brand-primary-tint) !important; + box-shadow: 0 0.5rem 1.2rem rgba(0, 0, 0, 0.4) !important; +} + +.quick-icon-circle { + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; } \ No newline at end of file diff --git a/app/templates/admin/audit_logs.html b/app/templates/admin/audit_logs.html new file mode 100644 index 0000000..18f10bc --- /dev/null +++ b/app/templates/admin/audit_logs.html @@ -0,0 +1,161 @@ +{% extends "base.html" %} + +{% block title %}{% trans %}Bitácora de Auditoría - Edu-Space Admin{% endtrans %}{% endblock %} + +{% block content %} +
+ +
+
+

+ {% trans %}Registro de Auditoría Institucional{% endtrans %} +

+

{% trans %}Trazabilidad completa de operaciones, modificaciones y eventos de seguridad{% endtrans %}

+
+
+ + {% trans %}Volver al Panel{% endtrans %} + +
+
+ + +
+
+
+
+
+ + +
+
+
+ +
+
+ +
+
+ + {% if search or current_module or current_action %} + + + + {% endif %} +
+
+
+
+ + +
+
+ {% if audit_logs %} +
+ + + + + + + + + + + + + + {% for log in audit_logs %} + + + + + + + + + + {% endfor %} + +
{% trans %}Fecha / Hora{% endtrans %}{% trans %}Usuario{% endtrans %}{% trans %}Acción{% endtrans %}{% trans %}Módulo{% endtrans %}{% trans %}ID Entidad{% endtrans %}{% trans %}Detalles{% endtrans %}{% trans %}IP{% endtrans %}
+ {{ log.created_at.strftime('%Y-%m-%d %H:%M:%S') }} + +
{{ log.user_email or 'Sistema' }}
+
+ {% if log.action == 'CREATE' %} + {{ log.action }} + {% elif log.action == 'UPDATE' %} + {{ log.action }} + {% elif log.action == 'DELETE' %} + {{ log.action }} + {% elif 'LOGIN' in log.action %} + {{ log.action }} + {% else %} + {{ log.action }} + {% endif %} + + {{ log.module }} + + {{ log.entity_id or '-' }} + + {{ log.details or '-' }} + + {{ log.ip_address or '-' }} +
+
+ + + {% if pagination.pages > 1 %} +
+ + {% trans %}Mostrando página{% endtrans %} {{ pagination.page }} {% trans %}de{% endtrans %} {{ pagination.pages }} ({{ pagination.total }} {% trans %}registros{% endtrans %}) + + +
+ {% endif %} + + {% else %} +
+ +
{% trans %}No se encontraron registros de auditoría{% endtrans %}
+

{% trans %}Los eventos del sistema se registrarán automáticamente a medida que ocurran cambios.{% endtrans %}

+
+ {% endif %} +
+
+
+{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index c99e669..caf4290 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -46,92 +46,171 @@