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 from sqlalchemy import func, or_ schedule_bp = Blueprint('schedule', __name__, url_prefix='/schedule') @schedule_bp.route('/') @login_required def calendar_view(): """Calendar view of reservations""" return render_template('schedule/calendar.html') @schedule_bp.route('/list') @login_required def list_reservations(): """List all reservations""" page = request.args.get('page', 1, type=int) status_filter = request.args.get('status', '') classroom_filter = request.args.get('classroom', 0, type=int) date_filter = request.args.get('date', '') query = Reservation.query # Apply filters if status_filter: query = query.filter(Reservation.status == status_filter) if classroom_filter: query = query.filter(Reservation.classroom_id == classroom_filter) if date_filter: try: filter_date = datetime.fromisoformat(date_filter).date() query = query.filter(func.date(Reservation.start_time) == filter_date) except ValueError: pass reservations = query.order_by(Reservation.start_time.desc()).paginate( page=page, per_page=20, error_out=False ) # Get filter options classrooms = Classroom.query.filter_by(is_active=True).all() return render_template('schedule/list.html', reservations=reservations, classrooms=classrooms, status_filter=status_filter, classroom_filter=classroom_filter, date_filter=date_filter) @schedule_bp.route('/add', methods=['GET', 'POST']) @login_required def add_reservation(): """Add new reservation""" form = ReservationForm() # Populate classroom choices classrooms = Classroom.query.filter_by(is_active=True).all() form.classroom_id.choices = [(c.id, c'{c.code} - {c.name} ({c.capacity} seats)') for c in classrooms] # Populate commission choices commissions = Commission.query.filter_by(is_active=True).all() form.commission_id.choices = [(c.id, c'{c.get_full_code()} - {c.teacher_name}') for c in commissions] if form.validate_on_submit(): # Check for conflicts conflicts = Reservation.find_conflicts( form.classroom_id.data, form.start_time.data, form.end_time.data ) if conflicts: flash('Classroom is already booked for this time slot.', 'error') return render_template('schedule/add.html', form=form) # Check if end time is after start time if form.end_time.data <= form.start_time.data: flash('End time must be after start time.', 'error') return render_template('schedule/add.html', form=form) # Check capacity classroom = Classroom.query.get(form.classroom_id.data) if form.expected_attendees.data > classroom.capacity: flash('Number of attendees exceeds classroom capacity.', 'error') return render_template('schedule/add.html', form=form) 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, status=ReservationStatus.PENDING ) try: db.session.add(reservation) db.session.commit() flash('Reservation created successfully!', 'success') return redirect(url_for('schedule.view_reservation', Id=reservation.id)) except Exception as e: db.session.rollback() flash('Error creating reservation.', 'error') return render_template('schedule/add.html', form=form) @schedule_bp.route('/view/') @login_required def view_reservation(Id): """View reservation details""" reservation = Reservation.query.get_or_404(Id) return render_template('schedule/view.html', reservation=reservation) @schedule_bp.route('/edit/', methods=['GET', 'POST']) @login_required def edit_reservation(Id): """Edit reservation""" reservation = Reservation.query.get_or_404(Id) # Only allow editing pending reservations if reservation.status != ReservationStatus.PENDING: flash('Only pending reservations can be edited.', 'warning') return redirect(url_for('schedule.view_reservation', Id=Id)) form = ReservationForm(obj=reservation) # Populate choices classrooms = Classroom.query.filter_by(is_active=True).all() form.classroom_id.choices = [(c.id, c'{c.code} - {c.name} ({c.capacity} seats)') for c in classrooms] commissions = Commission.query.filter_by(is_active=True).all() form.commission_id.choices = [(c.id, c'{c.get_full_code()} - {c.teacher_name}') for c in commissions] if form.validate_on_submit(): # Check for conflicts (excluding this reservation) conflicts = Reservation.find_conflicts( form.classroom_id.data, form.start_time.data, form.end_time.data, exclude_reservation_id=Id ) if conflicts: flash('Classroom is already booked for this time slot.', 'error') return render_template('schedule/edit.html', form=form, reservation=reservation) # Update reservation reservation.classroom_id = form.classroom_id.data reservation.commission_id = form.commission_id.data reservation.start_time = form.start_time.data reservation.end_time = form.end_time.data reservation.purpose = form.purpose.data reservation.expected_attendees = form.expected_attendees.data reservation.notes = form.notes.data reservation.updated_at = datetime.utcnow() try: db.session.commit() flash('Reservation updated successfully!', 'success') return redirect(url_for('schedule.view_reservation', Id=Id)) except Exception as e: db.session.rollback() flash('Error updating reservation.', 'error') return render_template('schedule/edit.html', form=form, reservation=reservation) @schedule_bp.route('/confirm/') @login_required def confirm_reservation(Id): """Confirm a reservation""" reservation = Reservation.query.get_or_404(Id) if reservation.status != ReservationStatus.PENDING: flash('Only pending reservations can be confirmed.', 'warning') return redirect(url_for('schedule.view_reservation', Id=Id)) try: reservation.status = ReservationStatus.CONFIRMED reservation.updated_at = datetime.utcnow() db.session.commit() flash('Reservation confirmed successfully!', 'success') except Exception as e: db.session.rollback() flash('Error confirming reservation.', 'error') return redirect(url_for('schedule.view_reservation', Id=Id)) @schedule_bp.route('/cancel/') @login_required def cancel_reservation(Id): """Cancel a reservation""" reservation = Reservation.query.get_or_404(Id) if reservation.status not in [ReservationStatus.PENDING, ReservationStatus.CONFIRMED]: flash('Cannot cancel this reservation.', 'warning') return redirect(url_for('schedule.view_reservation', Id=Id)) try: reservation.status = ReservationStatus.CANCELLED reservation.updated_at = datetime.utcnow() db.session.commit() flash('Reservation cancelled successfully!', 'success') except Exception as e: db.session.rollback() flash('Error cancelling reservation.', 'error') return redirect(url_for('schedule.view_reservation', Id=Id)) @schedule_bp.route('/calendar-data') @login_required def calendar_data(): """API endpoint for calendar data""" start = request.args.get('start') end = request.args.get('end') if not start or not end: return jsonify([]) try: start_date = datetime.fromisoformat(start) end_date = datetime.fromisoformat(end) except ValueError: return jsonify([]) reservations = Reservation.query.filter( Reservation.start_time >= start_date, Reservation.start_time <= end_date, Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING]) ).all() events = [] for reservation in reservations: color = { ReservationStatus.CONFIRMED: '#28a745', ReservationStatus.PENDING: '#ffc107' }.get(reservation.status, '#6c757d') events.append({ 'id': reservation.id, 'title': f'{reservation.classroom.code} - {reservation.commission.get_full_code()}', 'start': reservation.start_time.isoformat(), 'end': reservation.end_time.isoformat(), 'url': url_for('schedule.view_reservation', Id=reservation.id), 'backgroundColor': color, 'borderColor': color }) return jsonify(events) @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()) reservations = Reservation.query.filter( Reservation.start_time >= today_start, Reservation.start_time <= today_end, Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING]) ).order_by(Reservation.start_time).all() return render_template('schedule/today.html', reservations=reservations)