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('/') @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('//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('//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