333 lines
12 KiB
Python
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])
|