277 lines
10 KiB
Python
277 lines
10 KiB
Python
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, f'{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, f'{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/<int:Id>')
|
|
@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/<int:Id>', 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, f'{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, f'{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/<int:Id>')
|
|
@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/<int:Id>')
|
|
@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) |