from flask import Blueprint, jsonify, request from app.utils.jwt_decorators import jwt_required from app.models.classroom import Classroom from app.models.subject import Subject from app.models.user import User from app.models.reservation import Reservation from app.models.audit_log import AuditLog from datetime import datetime, timedelta from sqlalchemy import func from app import db from app.models.milestone import AcademicMilestone from app.models.subject import Commission api_dashboard_bp = Blueprint('api_dashboard', __name__, url_prefix='/api/v1/dashboard') def serialize_reservation(r): subject_name = (r.commission.subject.name if (r.commission and r.commission.subject) else r.purpose) or 'Actividad Académica' subject_code = (r.commission.subject.code if (r.commission and r.commission.subject) else '') classroom_code = r.classroom.code if r.classroom else 'Sin Aula' is_virtual = ('virtual' in classroom_code.lower()) or (getattr(r.classroom, 'is_virtual', False) if r.classroom else False) return { 'id': r.id, 'start_time': r.start_time.strftime('%H:%M') if r.start_time else '', 'end_time': r.end_time.strftime('%H:%M') if r.end_time else '', 'date_display': r.start_time.strftime('%d/%m') if r.start_time else '', 'classroom': { 'id': r.classroom_id, 'code': classroom_code, 'is_virtual': is_virtual, 'location_display': getattr(r.classroom, 'location_display', f'Sede Central · {classroom_code}') if r.classroom else 'Sin Aula' }, 'subject_name': subject_name, 'subject_code': subject_code, 'shift': r.shift or 'Mañana', 'commission': { 'id': r.commission_id, 'code': r.commission.code if r.commission else 'COM-1' }, 'status': str(r.status.value if hasattr(r.status, 'value') else r.status), 'user': { 'id': r.user_id, 'name': r.user.name if r.user else 'Docente' }, 'virtual_link': r.virtual_link or ('https://meet.google.com/edu-space-' + classroom_code.lower().replace(' ', '-') if is_virtual else '') } def serialize_milestone(m): return { 'id': m.id, 'title': m.title, 'materia_codigo': m.materia_codigo, 'subject_name': m.subject.name if (hasattr(m, 'subject') and m.subject) else m.materia_codigo, 'date': m.date.strftime('%d/%m/%Y') if m.date else '', 'type_obj': { 'name': m.type_obj.name if m.type_obj else 'Evaluación', 'color': m.type_obj.color if m.type_obj else '#0d6efd' }, 'classroom': m.classroom or 'Sede Central', 'link': m.link or '', 'notes': m.notes or '' } @api_dashboard_bp.route('/stats', methods=['GET']) @jwt_required def get_stats(): """Retorna estadísticas clave y datos operativos para el Dashboard.""" today = datetime.utcnow().date() try: total_classrooms = Classroom.query.filter_by(is_active=True).count() total_subjects = Subject.query.filter_by(is_active=True).count() total_users = User.query.filter_by(is_active=True).count() # Aulas presenciales y virtuales virtual_classrooms = Classroom.query.filter_by(is_active=True).filter( (Classroom.room_number.ilike('%VIRTUAL%')) | (Classroom.building.ilike('%VIRTUAL%')) ).count() physical_classrooms = max(1, total_classrooms - virtual_classrooms) today_reservations_q = Reservation.query.filter( func.date(Reservation.start_time) == today ).order_by(Reservation.start_time.asc()).all() today_reservations = len(today_reservations_q) pending_reservations_q = Reservation.query.filter( Reservation.status == 'PENDING' ).order_by(Reservation.start_time.asc()).all() pending_count = len(pending_reservations_q) upcoming_reservations_q = Reservation.query.filter( Reservation.start_time >= datetime.utcnow() ).order_by(Reservation.start_time.asc()).limit(10).all() milestones_q = AcademicMilestone.query.filter( AcademicMilestone.is_active == True ).order_by(AcademicMilestone.date.asc()).limit(10).all() recent_audits = AuditLog.query.order_by(AuditLog.created_at.desc()).limit(6).all() audit_data = [{ 'created_at': a.created_at.strftime('%Y-%m-%d %H:%M') if a.created_at else None, 'action_type': a.action_type, 'ip_address': a.ip_address, 'user_email': getattr(a.user, 'email', 'System') if hasattr(a, 'user') else 'System' } for a in recent_audits] occupancy_rate = min(100, int((today_reservations / max(1, physical_classrooms * 3)) * 100)) if physical_classrooms > 0 else 0 pending_reservations = [serialize_reservation(r) for r in pending_reservations_q] today_schedule_list = [serialize_reservation(r) for r in today_reservations_q] upcoming_reservations = [serialize_reservation(r) for r in upcoming_reservations_q] milestones = [serialize_milestone(m) for m in milestones_q] except Exception as e: print(f"Error fetching stats: {e}") total_classrooms = 0 total_subjects = 0 total_users = 0 today_reservations = 0 pending_count = 0 physical_classrooms = 0 virtual_classrooms = 0 occupancy_rate = 0 audit_data = [] pending_reservations = [] today_schedule_list = [] upcoming_reservations = [] milestones = [] # Mocked monthly data for chart since complex SQL group by month might differ based on dialect (SQLite vs Postgres) monthly_data = [ {"month": "Ene", "reservations": 120}, {"month": "Feb", "reservations": 180}, {"month": "Mar", "reservations": 250}, {"month": "Abr", "reservations": 220}, {"month": "May", "reservations": 300}, {"month": "Jun", "reservations": 310}, {"month": "Jul", "reservations": 200}, {"month": "Ago", "reservations": 350}, {"month": "Sep", "reservations": 420}, {"month": "Oct", "reservations": 480}, {"month": "Nov", "reservations": 520}, {"month": "Dic", "reservations": 210} ] return jsonify({ 'status': 'success', 'stats': { 'total_classrooms': total_classrooms, 'active_subjects': total_subjects, 'total_active_users': total_users, 'today_reservations': today_reservations, 'pending_count': pending_count, 'physical_classrooms': physical_classrooms, 'virtual_classrooms': virtual_classrooms, 'occupancy_rate': occupancy_rate }, 'audit_logs': audit_data, 'monthly_data': monthly_data, 'pending_reservations': pending_reservations, 'today_schedule_list': today_schedule_list, 'upcoming_reservations': upcoming_reservations, 'milestones': milestones }), 200 @api_dashboard_bp.route('/sync-sheets', methods=['POST']) @jwt_required def sync_google_sheets(): """Ejecuta la sincronización real con las hojas de cálculo de Google Sheets.""" from app.services.sheets_importer import GoogleSheetsImporter try: importer = GoogleSheetsImporter() stats = importer.sync() return jsonify({ 'status': 'success', 'stats': stats }), 200 except Exception as e: return jsonify({ 'status': 'error', 'message': str(e) }), 500