from flask import Blueprint, render_template, jsonify, redirect, url_for, session, request, flash from flask_login import login_required, current_user from app import db from app.models.reservation import Reservation from app.models.classroom import Classroom from app.models.subject import Subject from app.models.subject import Commission from datetime import datetime, timedelta from sqlalchemy import func from flask_babel import gettext as _ main_bp = Blueprint('main', __name__) @main_bp.route('/') @login_required def dashboard(): """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 Exception: total_classrooms = 0 try: total_subjects = Subject.query.filter_by(is_active=True).count() except Exception: total_subjects = 0 try: today_reservations = Reservation.query.filter( func.date(Reservation.start_time) == today ).count() except Exception: today_reservations = 0 week_start = today - timedelta(days=today.weekday()) week_end = week_start + timedelta(days=6) try: week_reservations = Reservation.query.filter( func.date(Reservation.start_time) >= week_start, func.date(Reservation.start_time) <= week_end ).count() except Exception: week_reservations = 0 stats = { 'total_classrooms': total_classrooms, 'total_subjects': total_subjects, 'today_reservations': today_reservations, 'week_reservations': week_reservations } 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') @login_required def dashboard_stats_api(): """API endpoint for dashboard statistics""" # Get today's data today = datetime.utcnow().date() today_reservations = Reservation.query.filter( func.date(Reservation.start_time) == today ).count() # Get this week's data week_start = today - timedelta(days=today.weekday()) week_end = week_start + timedelta(days=6) week_reservations = Reservation.query.filter( func.date(Reservation.start_time) >= week_start, func.date(Reservation.start_time) <= week_end ).count() # Get monthly data for chart current_date = datetime.utcnow() monthly_data = [] for i in range(12): month_date = current_date - timedelta(days=30 * i) month_start = month_date.replace(day=1) month_end = (month_start + timedelta(days=32)).replace(day=1) - timedelta(days=1) monthly_reservations = Reservation.query.filter( func.date(Reservation.start_time) >= month_start.date(), func.date(Reservation.start_time) <= month_end.date() ).count() monthly_data.append({ 'month': month_start.strftime('%Y-%m'), 'reservations': monthly_reservations }) return jsonify({ 'today_reservations': today_reservations, 'week_reservations': week_reservations, 'monthly_data': list(reversed(monthly_data)) }) @main_bp.route('/genetic-optimizer') @login_required def genetic_optimizer(): """Serve the genetic algorithm optimization interface""" if not (current_user.has_permission('optimizer', 'read') or (current_user.role and current_user.role.upper() == 'ADMIN')): flash(_('No tienes permisos para acceder a la herramienta de optimización.'), 'danger') return redirect(url_for('main.dashboard')) return render_template('genetic_optimizer.html') @main_bp.route('/set_language/') def set_language(language=None): if language not in ['en', 'es']: language = 'es' session['language'] = language # Si se selecciona el idioma desde la pantalla de login, registrar la selección explícita if request.referrer and 'login' in request.referrer: session['login_lang_explicit'] = language if current_user.is_authenticated: current_user.preferred_language = language db.session.commit() return redirect(request.referrer or url_for('main.dashboard')) @main_bp.route('/api/milestone-types') def api_milestone_types(): """Retorna las tipificaciones de hitos activas en JSON para consumo de frontend/mobile""" from app.models.milestone import MilestoneType, BASE_MILESTONE_TYPES if MilestoneType.query.count() == 0: for item in BASE_MILESTONE_TYPES: mt = MilestoneType( name=item['name'], code=item['code'], color=item['color'], description=item['description'], sort_order=item['sort_order'], is_active=True ) db.session.add(mt) db.session.commit() types = MilestoneType.query.filter_by(is_active=True).order_by(MilestoneType.sort_order.asc(), MilestoneType.id.asc()).all() return jsonify([t.to_dict() for t in types])