184 lines
7.5 KiB
Python
184 lines
7.5 KiB
Python
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
|
|
|