from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify from flask_login import login_required, current_user from app import db from app.models.reservation import Reservation, ReservationStatus from app.models.classroom import Classroom from app.models.subject import Subject, Commission from app.forms.reservation import ReservationForm from datetime import datetime, timedelta, date from sqlalchemy import func, or_ from sqlalchemy.orm import joinedload schedule_bp = Blueprint('schedule', __name__, url_prefix='/schedule') @schedule_bp.route('/today') @login_required def today_schedule(): """Show today's schedule""" today = datetime.utcnow().date() today_start = datetime.combine(today, datetime.min.time()) today_end = datetime.combine(today, datetime.max.time()) # Get reservations with eager loading reservations = Reservation.query.options( joinedload(Reservation.classroom), joinedload(Reservation.commission), joinedload(Reservation.user) ).filter( Reservation.start_time >= today_start, Reservation.start_time <= today_end, Reservation.status.in_([ReservationStatus.CONFIRMED.value, ReservationStatus.PENDING.value]) ).order_by(Reservation.start_time).all() # Create time blocks for timeline view time_blocks = {} for reservation in reservations: start_hour = reservation.start_time.hour end_hour = reservation.end_time.hour for hour in range(max(7, start_hour), min(22, end_hour + 1)): if hour not in time_blocks: time_blocks[hour] = [] if reservation not in time_blocks[hour]: time_blocks[hour].append({ 'reservation': reservation, 'classroom': reservation.classroom, 'commission': reservation.commission, 'user': reservation.user }) # Calculate stats total_reservations = len(reservations) confirmed = len([r for r in reservations if r.status == ReservationStatus.CONFIRMED.value]) pending = len([r for r in reservations if r.status == ReservationStatus.PENDING]) # Get total classrooms and calculate occupied ones from app.models.classroom import Classroom total_classrooms = Classroom.query.count() occupied_classrooms = len(set(r.classroom_id for r in reservations if r.classroom_id)) stats = { 'total_reservations': total_reservations, 'confirmed': confirmed, 'pending': pending, 'occupied_classrooms': occupied_classrooms, 'total_classrooms': total_classrooms } return render_template('schedule/today.html', reservations=reservations, time_blocks=time_blocks, stats=stats, today=today) @schedule_bp.route('/cancel/', methods=['POST']) @login_required def cancel_reservation(id): """Cancel a reservation""" try: reservation = Reservation.query.get_or_404(id) if not reservation.can_be_cancelled(): flash('Reservations can only be cancelled at least 24 hours before start time.', 'error') return redirect(request.referrer) reservation.status = ReservationStatus.CANCELLED db.session.commit() flash('Reservation cancelled successfully!', 'success') except Exception as e: db.session.rollback() flash(f'Error cancelling reservation: {str(e)}', 'error') return redirect(request.referrer) @schedule_bp.route('/reservation//view') @login_required def view_reservation(id): """View reservation details""" try: reservation = Reservation.query.options( joinedload(Reservation.classroom), joinedload(Reservation.commission), joinedload(Reservation.user) ).get_or_404(id) return render_template('schedule/view.html', reservation=reservation, title='Reservation Details') except Exception as e: flash(f'Error loading reservation details: {str(e)}', 'error') return redirect(url_for('schedule.today_schedule')) @schedule_bp.route('/calendar') @login_required def calendar_view(): """Show calendar view""" return render_template('schedule/calendar.html', title='Calendar View') @schedule_bp.route('/commissions') def commissions_view(): subjects = Subject.query.all() # Calculamos el total de capacidad sumando todas las comisiones de todas las materias total_capacity = sum(c.max_students for s in subjects for c in s.commissions) # Calculamos el total de comisiones total_commissions = sum(len(s.commissions) for s in subjects) return render_template('schedule/commissions.html', subjects=subjects, total_capacity=total_capacity, total_commissions=total_commissions) @schedule_bp.route('/add', methods=['GET', 'POST']) @login_required def add_reservation(): """Add new reservation""" from app.models.classroom import Classroom from app.models.subject import Commission form = ReservationForm() # Get active classrooms for the dropdown classrooms = Classroom.query.filter_by(is_active=True).all() # Get active commissions for the dropdown commissions = Commission.query.filter_by(active=True).all() # Populate form choices form.classroom_id.choices = [(c.id, f'{c.code_display} - {c.name} (Cap: {c.capacity})') for c in classrooms] form.commission_id.choices = [(c.id, f'{c.get_full_code()} - {c.subject.name if c.subject else "No Subject"} (Students: {c.max_students})') for c in commissions] if request.method == 'POST': if form.validate_on_submit(): try: # Check for existing reservations at the same time from app.models.reservation import ReservationStatus existing_reservation = Reservation.query.filter_by( classroom_id=form.classroom_id.data, status=ReservationStatus.CONFIRMED.value ).filter( Reservation.start_time < form.end_time.data, Reservation.end_time > form.start_time.data ).first() if existing_reservation: flash('This classroom is already booked for the selected time slot.', 'danger') return render_template('schedule/add.html', form=form, title='New Reservation', classroom_list=classrooms, commission_list=commissions) # Create new reservation reservation = Reservation( classroom_id=form.classroom_id.data, commission_id=form.commission_id.data, user_id=current_user.id, start_time=form.start_time.data, end_time=form.end_time.data, purpose=form.purpose.data, notes=form.notes.data, status=ReservationStatus.PENDING.value, expected_attendees=form.expected_attendees.data ) db.session.add(reservation) db.session.commit() flash('Reservation request submitted successfully! It will be reviewed by an administrator.', 'success') return redirect(url_for('schedule.list_reservations')) except Exception as e: db.session.rollback() current_app.logger.error(f"Error creating reservation: {str(e)}") flash(f'Error creating reservation: {str(e)}', 'danger') return render_template('schedule/add.html', form=form, title='New Reservation', classroom_list=classrooms, commission_list=commissions) @schedule_bp.route('/list') @login_required def list_reservations(): """List all reservations""" return redirect(url_for('schedule.today_schedule')) @schedule_bp.route('/reservation//confirm', methods=['GET', 'POST']) @login_required def confirm_reservation(id): """Confirm a reservation (Handles both GET from link and POST from form)""" try: reservation = Reservation.query.get_or_404(id) # Validación de seguridad: Solo confirmar si está pendiente if reservation.status != ReservationStatus.PENDING.value: flash('Only pending reservations can be confirmed.', 'warning') return redirect(url_for('schedule.today_schedule')) reservation.status = ReservationStatus.CONFIRMED.value db.session.commit() flash( f'Reservation for {reservation.commission.code if reservation.commission else "Class"} confirmed successfully!', 'success') except Exception as e: db.session.rollback() flash(f'Error confirming reservation: {str(e)}', 'error') # Redirigir a la página desde la que venía el usuario o al inicio de hoy return redirect(request.referrer or url_for('schedule.today_schedule')) # API endpoints @schedule_bp.route('/api/classroom-availability') @login_required def classroom_availability_api(): """Get classroom availability for a specific date/time""" try: date_str = request.args.get('date') start_time_str = request.args.get('start_time') end_time_str = request.args.get('end_time') max_students = request.args.get('max_students', type=int) if not all([date_str, start_time_str, end_time_str]): return jsonify({'error': 'Missing required parameters: date, start_time, end_time'}), 400 # Parse datetime strings from datetime import datetime start_time = datetime.strptime(f"{date_str} {start_time_str}", "%Y-%m-%d %H:%M") end_time = datetime.strptime(f"{date_str} {end_time_str}", "%Y-%m-%d %H:%M") # Get availability status from app.models.classroom import Classroom availability = Classroom.get_availability_status(start_time, end_time) # Get best classroom recommendation if max_students provided best_recommendation = None if max_students: best_classrooms = Classroom.find_best_classroom(max_students, start_time, end_time) if best_classrooms: best_recommendation = best_classrooms[0] # Most efficient one # Format response response = { 'availability': { 'available': [ { 'id': item['classroom'].id, 'code': item['classroom'].code_display, 'name': item['classroom'].name, 'capacity': item['classroom'].capacity, 'location': item['classroom'].get_full_location() } for item in availability['available'] ], 'occupied': [ { 'id': item['classroom'].id, 'code': item['classroom'].code_display, 'name': item['classroom'].name, 'capacity': item['classroom'].capacity, 'location': item['classroom'].get_full_location(), 'conflicts': [ { 'start_time': conflict.start_time.strftime('%H:%M'), 'end_time': conflict.end_time.strftime('%H:%M'), 'commission': conflict.commission.get_full_code() if conflict.commission else 'Unknown' } for conflict in item['conflicts'] ] } for item in availability['occupied'] ], 'stats': { 'total_available': availability['total_available'], 'total_occupied': availability['total_occupied'] } } } if best_recommendation: response['recommendation'] = { 'classroom': { 'id': best_recommendation['classroom'].id, 'code': best_recommendation['classroom'].code_display, 'name': best_recommendation['classroom'].name, 'capacity': best_recommendation['classroom'].capacity, 'location': best_recommendation['classroom'].get_full_location() }, 'efficiency': best_recommendation['efficiency'], 'wasted_capacity': best_recommendation['wasted_capacity'], 'reason': 'Best match based on capacity and availability' } return jsonify(response) except ValueError as e: return jsonify({'error': f'Invalid date/time format: {str(e)}'}), 400 except Exception as e: return jsonify({'error': str(e)}), 500 @schedule_bp.route('/calendar-data') @login_required def calendar_data(): """Get calendar events data for FullCalendar""" try: # Get date range from query parameters start = request.args.get('start') end = request.args.get('end') if start and end: from datetime import datetime start_date = datetime.fromisoformat(start) end_date = datetime.fromisoformat(end) else: # Default to current month today = date.today() start_date = datetime.combine(today.replace(day=1), datetime.min.time()) next_month = today.replace(day=28) + timedelta(days=4) # Next month end_date = datetime.combine(next_month.replace(day=1), datetime.max.time()) # Get reservations in the date range reservations = Reservation.query.options( joinedload(Reservation.classroom), joinedload(Reservation.commission) ).filter( Reservation.start_time >= start_date, Reservation.start_time <= end_date, Reservation.status.in_([ReservationStatus.CONFIRMED.value, ReservationStatus.PENDING.value]) ).order_by(Reservation.start_time).all() events = [] for reservation in reservations: events.append({ 'id': reservation.id, 'title': f"{reservation.commission.get_full_code() if reservation.commission else 'No Commission'} - {reservation.classroom.code_display}", 'start': reservation.start_time.isoformat(), 'end': reservation.end_time.isoformat(), 'url': url_for('schedule.view_reservation', id=reservation.id) if reservation.id else None, 'className': f'fc-event-{reservation.status.value.lower()}', 'extendedProps': { 'classroom': reservation.classroom.code_display if reservation.classroom else 'Unknown', 'commission': reservation.commission.get_full_code() if reservation.commission else 'No Commission', 'purpose': reservation.purpose or 'No purpose', 'attendees': reservation.expected_attendees, 'status': reservation.status.value, 'notes': reservation.notes or '' } }) return jsonify(events) except Exception as e: return jsonify({'error': str(e)}), 500 @schedule_bp.route('/api/today-events') @login_required def api_today_events(): """Get today's events as JSON""" try: today_start = datetime.combine(date.today(), datetime.min.time()) today_end = datetime.combine(date.today(), datetime.max.time()) reservations = Reservation.query.options( joinedload(Reservation.classroom), joinedload(Reservation.commission) ).filter( Reservation.start_time >= today_start, Reservation.start_time <= today_end, Reservation.status == ReservationStatus.CONFIRMED.value ).order_by(Reservation.start_time).all() events = [] for reservation in reservations: events.append({ 'id': reservation.id, 'title': f"{reservation.commission.get_full_code() if reservation.commission else 'No Commission'} - {reservation.classroom.code_display}", 'start': reservation.start_time.isoformat(), 'end': reservation.end_time.isoformat(), 'url': url_for('schedule.view_reservation', id=reservation.id) if reservation.id else None, 'extendedProps': { 'classroom': reservation.classroom.code_display if reservation.classroom else 'Unknown', 'purpose': reservation.purpose or 'No purpose', 'attendees': reservation.expected_attendees, 'status': reservation.status.value } }) return jsonify({'events': events}) except Exception as e: return jsonify({'error': str(e)}), 500