from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify from flask_login import login_required, current_user from app.models.reservation import Reservation, ReservationStatus from app.models.classroom import Classroom from app.models.subject import Commission from app.forms.reservation import ReservationForm from app import db from datetime import datetime, timedelta, date from wtforms import SelectField, StringField, TextAreaField, IntegerField, SubmitField, DateTimeLocalField from wtforms.validators import DataRequired, Length, NumberRange, Optional schedule_bp = Blueprint('schedule', __name__) @schedule_bp.route('/add', methods=['GET', 'POST']) @login_required def add_reservation(): """Add a new reservation""" form = ReservationForm() # Populate form choices form.classroom_id.choices = [(c.id, f"{c.code_display} (Capacity: {c.capacity})") for c in Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all()] form.commission_id.choices = [(c.id, c.get_full_code()) for c in Commission.query.filter(Commission.active == True).order_by(Commission.code).all()] if form.validate_on_submit(): try: # Check for time conflicts conflicting_reservations = Reservation.find_conflicts( form.classroom_id.data, form.start_time.data, form.end_time.data ) if conflicting_reservations: flash('This classroom is already reserved for the selected time slot.', 'error') # Get classrooms for the template classroom_list = Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all() return render_template('schedule/add.html', form=form, classroom_list=classroom_list, title='New Reservation') # Check capacity classroom = Classroom.query.get(form.classroom_id.data) if form.expected_attendees.data > classroom.capacity: flash(f'Expected attendees ({form.expected_attendees.data}) exceeds classroom capacity ({classroom.capacity}).', 'error') # Get classrooms for the template classroom_list = Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all() return render_template('schedule/add.html', form=form, classroom_list=classroom_list, title='New Reservation') # Create 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, expected_attendees=form.expected_attendees.data, notes=form.notes.data ) db.session.add(reservation) db.session.commit() flash('Reservation created successfully! It will be confirmed after review.', 'success') return redirect(url_for('schedule.list_reservations')) except Exception as e: db.session.rollback() flash(f'Error creating reservation: {str(e)}', 'error') # Get classrooms for the template classroom_list = Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all() return render_template('schedule/add.html', form=form, classroom_list=classroom_list, title='New Reservation') @schedule_bp.route('/list') @login_required def list_reservations(): """List all reservations""" try: page = request.args.get('page', 1, type=int) status_filter = request.args.get('status', 'all') date_from = request.args.get('date_from') date_to = request.args.get('date_to') # Build query query = Reservation.query # Apply filters if status_filter != 'all': try: query = query.filter_by(status=ReservationStatus[status_filter.upper()]) except KeyError: pass if date_from: try: from_date = datetime.strptime(date_from, '%Y-%m-%d') query = query.filter(Reservation.start_time >= from_date) except ValueError: pass if date_to: try: to_date = datetime.strptime(date_to, '%Y-%m-%d') + timedelta(days=1) query = query.filter(Reservation.start_time < to_date) except ValueError: pass # Order by start time descending reservations = query.order_by(Reservation.start_time.desc()).paginate( page=page, per_page=20, error_out=False ) return render_template('schedule/list.html', reservations=reservations, status_filter=status_filter, date_from=date_from, date_to=date_to, title='Reservations') except Exception as e: flash(f'Error loading reservations: {str(e)}', 'error') return render_template('schedule/list.html', reservations=None, title='Reservations') @schedule_bp.route('/today') @login_required def today_schedule(): """Show today's schedule""" try: today_start = datetime.combine(date.today(), datetime.min.time()) today_end = datetime.combine(date.today(), datetime.max.time()) # Get today's reservations reservations = Reservation.query.filter( Reservation.start_time >= today_start, Reservation.start_time <= today_end ).order_by(Reservation.start_time).all() # Group by time blocks for better display time_blocks = {} for reservation in reservations: hour_key = reservation.start_time.hour if hour_key not in time_blocks: time_blocks[hour_key] = [] time_blocks[hour_key].append({ 'reservation': reservation, 'classroom': reservation.classroom, 'commission': reservation.commission, 'user': reservation.user }) # Get quick stats stats = { 'total_reservations': len(reservations), 'confirmed': len([r for r in reservations if r.status == ReservationStatus.CONFIRMED]), 'pending': len([r for r in reservations if r.status == ReservationStatus.PENDING]), 'cancelled': len([r for r in reservations if r.status == ReservationStatus.CANCELLED]), 'total_classrooms': Classroom.query.filter_by(is_active=True).count(), 'occupied_classrooms': len(set(r.classroom_id for r in reservations if r.status == ReservationStatus.CONFIRMED)) } return render_template('schedule/today.html', time_blocks=time_blocks, reservations=reservations, stats=stats, today=date.today(), title='Today\'s Schedule') except Exception as e: flash(f'Error loading today\'s schedule: {str(e)}', 'error') return render_template('schedule/today.html', time_blocks={}, reservations=[], stats={}, today=date.today(), title='Today\'s Schedule') @schedule_bp.route('//confirm', methods=['POST']) @login_required def confirm_reservation(id): """Confirm a reservation""" reservation = Reservation.query.get_or_404(id) try: # Check if reservation can be confirmed if reservation.status != ReservationStatus.PENDING: flash('Only pending reservations can be confirmed.', 'error') return redirect(request.referrer) # Check for conflicts again before confirming conflicts = Reservation.find_conflicts( reservation.classroom_id, reservation.start_time, reservation.end_time ) if conflicts: flash('Cannot confirm: Time slot is already occupied.', 'error') return redirect(request.referrer) reservation.status = ReservationStatus.CONFIRMED db.session.commit() flash('Reservation confirmed successfully!', 'success') except Exception as e: db.session.rollback() flash(f'Error confirming reservation: {str(e)}', 'error') return redirect(request.referrer) @schedule_bp.route('//cancel', methods=['POST']) @login_required def cancel_reservation(id): """Cancel a reservation""" reservation = Reservation.query.get_or_404(id) try: # Check if reservation can be cancelled 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('/calendar') @login_required def calendar_view(): """Show calendar view""" return render_template('schedule/calendar.html', title='Calendar View') # API endpoints @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.filter( Reservation.start_time >= start_date, Reservation.start_time <= end_date, Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING]) ).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': f"/schedule/{reservation.id}", # For event clicking '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.filter( Reservation.start_time >= today_start, Reservation.start_time <= today_end, Reservation.status == ReservationStatus.CONFIRMED ).order_by(Reservation.start_time).all() events = [] for reservation in reservations: events.append({ 'id': reservation.id, 'title': f"{reservation.commission.get_full_code()} - {reservation.classroom.code_display}", 'start': reservation.start_time.isoformat(), 'end': reservation.end_time.isoformat(), 'extendedProps': { 'classroom': reservation.classroom.code_display, 'purpose': reservation.purpose, 'attendees': reservation.expected_attendees, 'status': reservation.status.value } }) return jsonify({'events': events}) except Exception as e: return jsonify({'error': str(e)}), 500