Files
admin-edu-space/app/routes/main.py
T

333 lines
12 KiB
Python

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/<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])