330 lines
14 KiB
Python
330 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 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('/<int:id>/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('/<int:id>/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 |