319 lines
14 KiB
Python
319 lines
14 KiB
Python
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
|
from flask_login import login_required, current_user
|
|
from datetime import datetime, date
|
|
from app.models.classroom import Classroom
|
|
from app.models.user import User
|
|
from app.forms.classroom import ClassroomForm
|
|
from app import db
|
|
from sqlalchemy import text
|
|
|
|
classrooms_bp = Blueprint('classrooms', __name__)
|
|
|
|
@classrooms_bp.route('/add', methods=['GET', 'POST'])
|
|
@login_required
|
|
def add_classroom():
|
|
"""Add a new classroom with dynamic building creation and floor suggestion"""
|
|
from app.models.building import Building
|
|
import re
|
|
from sqlalchemy import func
|
|
|
|
form = ClassroomForm()
|
|
buildings = Building.query.filter_by(is_active=True).order_by(Building.name.asc()).all()
|
|
form.building_id.choices = [(str(b.id), b.name) for b in buildings] + [('__new__', '+ Crear nuevo edificio')]
|
|
|
|
# Map of existing floors per building for datalist
|
|
building_floors = {str(b.id): list(dict.fromkeys(c.floor for c in b.classrooms if c.floor)) for b in buildings}
|
|
|
|
if form.validate_on_submit():
|
|
try:
|
|
building = None
|
|
if form.building_id.data == '__new__':
|
|
new_building_name = form.new_building_name.data.strip() if form.new_building_name.data else ''
|
|
if not new_building_name:
|
|
flash('Debe ingresar el nombre del nuevo edificio.', 'error')
|
|
return render_template('classrooms/add.html', form=form, building_floors=building_floors, title='Add Classroom')
|
|
|
|
building = Building.query.filter(func.lower(Building.name) == func.lower(new_building_name)).first()
|
|
if not building:
|
|
code_slug = re.sub(r'[^a-zA-Z0-9]+', '_', new_building_name.upper()).strip('_')
|
|
building = Building(name=new_building_name, code=code_slug, is_active=True)
|
|
db.session.add(building)
|
|
db.session.commit()
|
|
else:
|
|
building = Building.query.get_or_404(int(form.building_id.data))
|
|
|
|
room_code = form.room_number.data.strip()
|
|
floor_val = str(form.floor.data).strip()
|
|
|
|
# Prevent duplicate room in same building & floor
|
|
existing_classroom = Classroom.query.filter_by(
|
|
building_id=building.id,
|
|
code=room_code,
|
|
floor=floor_val
|
|
).first()
|
|
|
|
if existing_classroom:
|
|
flash(f'Ya existe un aula con el nombre "{room_code}" en el piso {floor_val} de {building.name}.', 'error')
|
|
return render_template('classrooms/add.html', form=form, building_floors=building_floors, title='Add Classroom')
|
|
|
|
cap_raw = str(form.capacity.data or '').strip()
|
|
if cap_raw in ['∞', 'inf', 'INF', 'Infinity', 'infinity', '0', 'ilimitada', 'Ilimitada', 'sin limite', 'Sin límite']:
|
|
cap_val = 0
|
|
else:
|
|
cap_val = int(cap_raw)
|
|
|
|
classroom = Classroom(
|
|
building_id=building.id,
|
|
building=building.name,
|
|
code=room_code,
|
|
floor=floor_val,
|
|
capacity=cap_val,
|
|
description=form.description.data,
|
|
is_active=form.is_active.data
|
|
)
|
|
|
|
db.session.add(classroom)
|
|
db.session.commit()
|
|
|
|
flash(f'Aula {classroom.location_display} creada exitosamente.', 'success')
|
|
return redirect(url_for('classrooms.list_classrooms'))
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
flash(f'Error creating classroom: {str(e)}', 'error')
|
|
|
|
return render_template('classrooms/add.html', form=form, building_floors=building_floors, title='Add Classroom')
|
|
|
|
|
|
@classrooms_bp.route('/', endpoint='index')
|
|
@classrooms_bp.route('/list')
|
|
@login_required
|
|
def list_classrooms():
|
|
"""List all classrooms with Building and Floor filters"""
|
|
try:
|
|
from app.models.building import Building
|
|
page = request.args.get('page', 1, type=int)
|
|
active_only = request.args.get('active_only', 'true').lower() == 'true'
|
|
search = request.args.get('search', '', type=str)
|
|
building_filter = request.args.get('building_id', '', type=str).strip()
|
|
floor_filter = request.args.get('floor', '', type=str).strip()
|
|
|
|
query = Classroom.query
|
|
|
|
if active_only:
|
|
query = query.filter_by(is_active=True)
|
|
|
|
if building_filter and building_filter.isdigit():
|
|
query = query.filter(Classroom.building_id == int(building_filter))
|
|
|
|
if floor_filter:
|
|
flr = str(floor_filter).strip()
|
|
if flr.upper() in ['0', 'PB', 'PLANTA BAJA']:
|
|
query = query.filter(db.or_(Classroom.floor == '0', Classroom.floor.ilike('%PB%'), Classroom.floor.ilike('%Planta Baja%')))
|
|
else:
|
|
query = query.filter(Classroom.floor == flr)
|
|
|
|
if search:
|
|
query = query.filter(
|
|
db.or_(
|
|
Classroom.building.ilike(f'%{search}%'),
|
|
Classroom.code.ilike(f'%{search}%'),
|
|
Classroom.description.ilike(f'%{search}%')
|
|
)
|
|
)
|
|
|
|
classrooms = query.order_by(Classroom.building, Classroom.floor, Classroom.code).paginate(
|
|
page=page, per_page=12, error_out=False
|
|
)
|
|
|
|
buildings = Building.query.filter_by(is_active=True).order_by(Building.name.asc()).all()
|
|
floors = [f[0] for f in db.session.query(Classroom.floor).filter(Classroom.is_active==True, Classroom.floor.isnot(None)).distinct().order_by(Classroom.floor.asc()).all()]
|
|
|
|
return render_template('classrooms/list.html',
|
|
classrooms=classrooms,
|
|
buildings=buildings,
|
|
floors=floors,
|
|
building_filter=building_filter,
|
|
floor_filter=floor_filter,
|
|
active_only=active_only,
|
|
search=search,
|
|
title='Classrooms')
|
|
|
|
except Exception as e:
|
|
flash(f'Error loading classrooms: {str(e)}', 'error')
|
|
return render_template('classrooms/list.html', classrooms=None, title='Classrooms')
|
|
|
|
|
|
@classrooms_bp.route('/<int:id>')
|
|
@login_required
|
|
def view_classroom(id):
|
|
"""View classroom details"""
|
|
try:
|
|
classroom = Classroom.query.get_or_404(id)
|
|
from app.models.reservation import Reservation, ReservationStatus
|
|
|
|
upcoming_reservations = Reservation.query.filter(
|
|
Reservation.classroom_id == id,
|
|
Reservation.start_time > datetime.utcnow(),
|
|
Reservation.status == ReservationStatus.CONFIRMED.value
|
|
).order_by(Reservation.start_time).limit(10).all()
|
|
|
|
return render_template('classrooms/view.html',
|
|
classroom=classroom,
|
|
upcoming_reservations=upcoming_reservations,
|
|
title=f'Classroom {classroom.code_display}')
|
|
|
|
except Exception as e:
|
|
flash(f'Error loading classroom: {str(e)}', 'error')
|
|
return redirect(url_for('classrooms.list_classrooms'))
|
|
|
|
|
|
@classrooms_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
|
|
@login_required
|
|
def edit_classroom(id):
|
|
"""Edit classroom with decoupled building and floor"""
|
|
from app.models.building import Building
|
|
import re
|
|
from sqlalchemy import func
|
|
|
|
classroom = Classroom.query.get_or_404(id)
|
|
form = ClassroomForm(obj=classroom)
|
|
|
|
buildings = Building.query.filter_by(is_active=True).order_by(Building.name.asc()).all()
|
|
form.building_id.choices = [(str(b.id), b.name) for b in buildings] + [('__new__', '+ Crear nuevo edificio')]
|
|
building_floors = {str(b.id): list(dict.fromkeys(c.floor for c in b.classrooms if c.floor)) for b in buildings}
|
|
|
|
if request.method == 'GET':
|
|
form.building_id.data = str(classroom.building_id or '')
|
|
form.room_number.data = classroom.code
|
|
form.floor.data = classroom.floor
|
|
if classroom.capacity == 0 or classroom.is_virtual:
|
|
form.capacity.data = '∞'
|
|
else:
|
|
form.capacity.data = str(classroom.capacity)
|
|
|
|
if form.validate_on_submit():
|
|
try:
|
|
building = None
|
|
if form.building_id.data == '__new__':
|
|
new_building_name = form.new_building_name.data.strip() if form.new_building_name.data else ''
|
|
if not new_building_name:
|
|
flash('Debe ingresar el nombre del nuevo edificio.', 'error')
|
|
return render_template('classrooms/edit.html', form=form, classroom=classroom, building_floors=building_floors, title='Edit Classroom')
|
|
|
|
building = Building.query.filter(func.lower(Building.name) == func.lower(new_building_name)).first()
|
|
if not building:
|
|
code_slug = re.sub(r'[^a-zA-Z0-9]+', '_', new_building_name.upper()).strip('_')
|
|
building = Building(name=new_building_name, code=code_slug, is_active=True)
|
|
db.session.add(building)
|
|
db.session.commit()
|
|
else:
|
|
building = Building.query.get_or_404(int(form.building_id.data))
|
|
|
|
room_code = form.room_number.data.strip()
|
|
floor_val = str(form.floor.data).strip()
|
|
|
|
# Check duplicate excluding current
|
|
existing_classroom = Classroom.query.filter(
|
|
Classroom.building_id == building.id,
|
|
Classroom.code == room_code,
|
|
Classroom.floor == floor_val,
|
|
Classroom.id != id
|
|
).first()
|
|
|
|
if existing_classroom:
|
|
flash(f'Ya existe otra aula con el nombre "{room_code}" en el piso {floor_val} de {building.name}.', 'error')
|
|
return render_template('classrooms/edit.html', form=form, classroom=classroom, building_floors=building_floors, title='Edit Classroom')
|
|
|
|
cap_raw = str(form.capacity.data or '').strip()
|
|
if cap_raw in ['∞', 'inf', 'INF', 'Infinity', 'infinity', '0', 'ilimitada', 'Ilimitada', 'sin limite', 'Sin límite']:
|
|
cap_val = 0
|
|
else:
|
|
cap_val = int(cap_raw)
|
|
|
|
classroom.building_id = building.id
|
|
classroom.building = building.name
|
|
classroom.code = room_code
|
|
classroom.floor = floor_val
|
|
classroom.capacity = cap_val
|
|
classroom.description = form.description.data
|
|
classroom.is_active = form.is_active.data
|
|
classroom.updated_at = datetime.utcnow()
|
|
|
|
db.session.commit()
|
|
|
|
flash(f'Aula {classroom.location_display} actualizada correctamente.', 'success')
|
|
return redirect(url_for('classrooms.view_classroom', id=classroom.id))
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
flash(f'Error updating classroom: {str(e)}', 'error')
|
|
|
|
return render_template('classrooms/edit.html', form=form, classroom=classroom, building_floors=building_floors, title='Edit Classroom')
|
|
|
|
@classrooms_bp.route('/<int:id>/toggle', methods=['POST'])
|
|
@login_required
|
|
def toggle_classroom(id):
|
|
"""Toggle classroom active status"""
|
|
classroom = Classroom.query.get_or_404(id)
|
|
|
|
try:
|
|
classroom.is_active = not classroom.is_active
|
|
classroom.updated_at = datetime.utcnow()
|
|
db.session.commit()
|
|
|
|
status = 'activated' if classroom.is_active else 'deactivated'
|
|
flash(f'Classroom {classroom.code_display} has been {status} successfully!', 'success')
|
|
|
|
except Exception as e:
|
|
db.session.rollback()
|
|
flash(f'Error updating classroom status: {str(e)}', 'error')
|
|
|
|
return redirect(request.referrer or url_for('classrooms.list_classrooms'))
|
|
|
|
# API endpoints for AJAX requests
|
|
@classrooms_bp.route('/api/available')
|
|
@login_required
|
|
def get_available_classrooms():
|
|
"""Get available classrooms for a specific time slot"""
|
|
start_time = request.args.get('start_time')
|
|
end_time = request.args.get('end_time')
|
|
|
|
if not start_time or not end_time:
|
|
return jsonify({'error': 'Start time and end time are required'}), 400
|
|
|
|
try:
|
|
from datetime import datetime
|
|
from app.models.reservation import Reservation, ReservationStatus
|
|
|
|
start_dt = datetime.fromisoformat(start_time)
|
|
end_dt = datetime.fromisoformat(end_time)
|
|
|
|
# Find all classrooms
|
|
all_classrooms = Classroom.query.filter_by(is_active=True).all()
|
|
available_classrooms = []
|
|
|
|
for classroom in all_classrooms:
|
|
# Check if classroom has conflicting reservations
|
|
conflicts = Reservation.query.filter(
|
|
Reservation.classroom_id == classroom.id,
|
|
Reservation.status == ReservationStatus.CONFIRMED.value,
|
|
Reservation.start_time < end_dt,
|
|
Reservation.end_time > start_dt
|
|
).all()
|
|
|
|
if not conflicts:
|
|
available_classrooms.append({
|
|
'id': classroom.id,
|
|
'code': classroom.code,
|
|
'building': classroom.building,
|
|
'room_number': classroom.code,
|
|
'floor': classroom.floor,
|
|
'capacity': classroom.capacity,
|
|
'code_display': classroom.code_display
|
|
})
|
|
|
|
return jsonify({'classrooms': available_classrooms})
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500 |