setup update 3
This commit is contained in:
+179
-161
@@ -1,214 +1,232 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_login import login_required
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from datetime import datetime, date
|
||||
from app.models.classroom import Classroom
|
||||
from app.models.user import User
|
||||
from app.forms.classroom import ClassroomForm
|
||||
from app import db
|
||||
from app.models.classroom import Classroom, ClassroomResource
|
||||
from app.models.reservation import Reservation, ReservationStatus
|
||||
from app.forms.classroom import ClassroomForm, ClassroomResourceForm
|
||||
from datetime import datetime
|
||||
from sqlalchemy import text
|
||||
|
||||
classrooms_bp = Blueprint('classrooms', __name__, url_prefix='/classrooms')
|
||||
|
||||
@classrooms_bp.route('/')
|
||||
@login_required
|
||||
def list_classrooms():
|
||||
"""List all classrooms"""
|
||||
page = request.args.get('page', 1, type=int)
|
||||
search = request.args.get('search', '')
|
||||
campus = request.args.get('campus', '')
|
||||
|
||||
query = Classroom.query.filter_by(is_active=True)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
Classroom.code.contains(search) |
|
||||
Classroom.name.contains(search) |
|
||||
Classroom.building.contains(search)
|
||||
)
|
||||
|
||||
if campus:
|
||||
query = query.filter(Classroom.campus_location == campus)
|
||||
|
||||
classrooms = query.paginate(
|
||||
page=page,
|
||||
per_page=20,
|
||||
error_out=False
|
||||
)
|
||||
|
||||
return render_template('classrooms/list.html',
|
||||
classrooms=classrooms,
|
||||
search=search,
|
||||
campus=campus)
|
||||
classrooms_bp = Blueprint('classrooms', __name__)
|
||||
|
||||
@classrooms_bp.route('/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_classroom():
|
||||
"""Add new classroom"""
|
||||
"""Add a new classroom"""
|
||||
form = ClassroomForm()
|
||||
|
||||
# TODO: Update when campus model is implemented
|
||||
# form.campus_id.choices = [(1, 'Main Campus')]
|
||||
|
||||
if form.validate_on_submit():
|
||||
classroom = Classroom(
|
||||
code=form.code.data,
|
||||
name=form.name.data,
|
||||
building=form.building.data,
|
||||
floor=form.floor.data,
|
||||
capacity=form.capacity.data,
|
||||
description=form.description.data,
|
||||
campus_location=form.campus_location.data
|
||||
)
|
||||
|
||||
try:
|
||||
existing_classroom = Classroom.query.filter_by(
|
||||
building=form.building.data,
|
||||
code=form.room_number.data,
|
||||
floor=form.floor.data
|
||||
).first()
|
||||
|
||||
if existing_classroom:
|
||||
flash('Classroom with this building, room number, and floor already exists.', 'error')
|
||||
return render_template('classrooms/add.html', form=form, title='Add Classroom')
|
||||
|
||||
classroom = Classroom(
|
||||
# campus_id=form.campus_id.data,
|
||||
building=form.building.data,
|
||||
code=form.room_number.data,
|
||||
name=f"{form.building.data}-{form.room_number.data}",
|
||||
floor=form.floor.data,
|
||||
capacity=form.capacity.data,
|
||||
description=form.description.data,
|
||||
is_active=form.is_active.data
|
||||
)
|
||||
|
||||
db.session.add(classroom)
|
||||
db.session.commit()
|
||||
flash(f'Classroom {classroom.code} added successfully!', 'success')
|
||||
|
||||
flash(f'Classroom {classroom.code_display} has been created successfully!', 'success')
|
||||
return redirect(url_for('classrooms.list_classrooms'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash('Error adding classroom. Classroom code might already exist.', 'error')
|
||||
flash(f'Error creating classroom: {str(e)}', 'error')
|
||||
|
||||
return render_template('classrooms/add.html', form=form)
|
||||
return render_template('classrooms/add.html', form=form, title='Add Classroom')
|
||||
|
||||
@classrooms_bp.route('/edit/<int:Id>', methods=['GET', 'POST'])
|
||||
@classrooms_bp.route('/list')
|
||||
@login_required
|
||||
def list_classrooms():
|
||||
"""List all classrooms"""
|
||||
try:
|
||||
# Get filters
|
||||
page = request.args.get('page', 1, type=int)
|
||||
active_only = request.args.get('active_only', 'true').lower() == 'true'
|
||||
search = request.args.get('search', '', type=str)
|
||||
|
||||
# Build query
|
||||
query = Classroom.query
|
||||
|
||||
if active_only:
|
||||
query = query.filter_by(is_active=True)
|
||||
|
||||
if search:
|
||||
query = query.filter(
|
||||
db.or_(
|
||||
Classroom.building.ilike(f'%{search}%'),
|
||||
Classroom.code.ilike(f'%{search}%'),
|
||||
Classroom.description.ilike(f'%{search}%')
|
||||
)
|
||||
)
|
||||
|
||||
# Paginate results
|
||||
classrooms = query.order_by(Classroom.building, Classroom.floor, Classroom.code).paginate(
|
||||
page=page, per_page=12, error_out=False
|
||||
)
|
||||
|
||||
return render_template('classrooms/list.html',
|
||||
classrooms=classrooms,
|
||||
active_only=active_only,
|
||||
search=search,
|
||||
title='Classrooms')
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Error loading classrooms: {str(e)}', 'error')
|
||||
return render_template('classrooms/list.html', classrooms=None, title='Classrooms')
|
||||
|
||||
@classrooms_bp.route('/<int:id>')
|
||||
@login_required
|
||||
def edit_classroom(Id):
|
||||
"""Edit classroom"""
|
||||
classroom = Classroom.query.get_or_404(Id)
|
||||
|
||||
if not classroom.is_active:
|
||||
flash('This classroom has been deactivated.', 'warning')
|
||||
def view_classroom(id):
|
||||
"""View classroom details"""
|
||||
try:
|
||||
classroom = Classroom.query.get_or_404(id)
|
||||
|
||||
# Get upcoming reservations for this classroom
|
||||
from app.models.reservation import Reservation, ReservationStatus
|
||||
|
||||
upcoming_reservations = Reservation.query.filter(
|
||||
Reservation.classroom_id == id,
|
||||
Reservation.start_time > datetime.utcnow(),
|
||||
Reservation.status == ReservationStatus.CONFIRMED
|
||||
).order_by(Reservation.start_time).limit(10).all()
|
||||
|
||||
return render_template('classrooms/view.html',
|
||||
classroom=classroom,
|
||||
upcoming_reservations=upcoming_reservations,
|
||||
title=f'Classroom {classroom.code_display}')
|
||||
|
||||
except Exception as e:
|
||||
flash(f'Error loading classroom: {str(e)}', 'error')
|
||||
return redirect(url_for('classrooms.list_classrooms'))
|
||||
|
||||
|
||||
@classrooms_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_classroom(id):
|
||||
"""Edit classroom"""
|
||||
classroom = Classroom.query.get_or_404(id)
|
||||
form = ClassroomForm(obj=classroom)
|
||||
|
||||
# TODO: Update when campus model is implemented
|
||||
# form.campus_id.choices = [(1, 'Main Campus')]
|
||||
|
||||
if form.validate_on_submit():
|
||||
try:
|
||||
classroom.code = form.code.data
|
||||
classroom.name = form.name.data
|
||||
# Check for duplicates (excluding current classroom)
|
||||
existing_classroom = Classroom.query.filter(
|
||||
Classroom.building == form.building.data,
|
||||
Classroom.code == form.room_number.data,
|
||||
Classroom.floor == form.floor.data,
|
||||
Classroom.id != id
|
||||
).first()
|
||||
|
||||
if existing_classroom:
|
||||
flash('Classroom with this building, room number, and floor already exists.', 'error')
|
||||
return render_template('classrooms/edit.html', form=form, classroom=classroom, title='Edit Classroom')
|
||||
|
||||
# Update classroom
|
||||
# classroom.campus_id = form.campus_id.data
|
||||
classroom.building = form.building.data
|
||||
classroom.code = form.room_number.data
|
||||
classroom.name = f"{form.building.data}-{form.room_number.data}"
|
||||
classroom.floor = form.floor.data
|
||||
classroom.capacity = form.capacity.data
|
||||
classroom.description = form.description.data
|
||||
classroom.campus_location = form.campus_location.data
|
||||
classroom.is_active = form.is_active.data
|
||||
classroom.updated_at = datetime.utcnow()
|
||||
|
||||
db.session.commit()
|
||||
flash(f'Classroom {classroom.code} updated successfully!', 'success')
|
||||
return redirect(url_for('classrooms.view_classroom', Id=classroom.id))
|
||||
|
||||
flash(f'Classroom {classroom.code_display} has been updated successfully!', 'success')
|
||||
return redirect(url_for('classrooms.view_classroom', id=classroom.id))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash('Error updating classroom.', 'error')
|
||||
flash(f'Error updating classroom: {str(e)}', 'error')
|
||||
|
||||
return render_template('classrooms/edit.html', form=form, classroom=classroom)
|
||||
return render_template('classrooms/edit.html', form=form, classroom=classroom, title='Edit Classroom')
|
||||
|
||||
@classrooms_bp.route('/view/<int:Id>')
|
||||
@classrooms_bp.route('/<int:id>/toggle', methods=['POST'])
|
||||
@login_required
|
||||
def view_classroom(Id):
|
||||
"""View classroom details"""
|
||||
classroom = Classroom.query.get_or_404(Id)
|
||||
|
||||
if not classroom.is_active:
|
||||
flash('This classroom has been deactivated.', 'warning')
|
||||
return redirect(url_for('classrooms.list_classrooms'))
|
||||
|
||||
# Get classroom resources
|
||||
resources = ClassroomResource.query.filter_by(classroom_id=Id).all()
|
||||
|
||||
# Get upcoming reservations
|
||||
upcoming_reservations = Reservation.query.filter(
|
||||
Reservation.classroom_id == Id,
|
||||
Reservation.start_time >= datetime.utcnow(),
|
||||
Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING])
|
||||
).order_by(Reservation.start_time).limit(10).all()
|
||||
|
||||
return render_template('classrooms/view.html',
|
||||
classroom=classroom,
|
||||
resources=resources,
|
||||
upcoming_reservations=upcoming_reservations)
|
||||
|
||||
@classrooms_bp.route('/delete/<int:Id>')
|
||||
@login_required
|
||||
def delete_classroom(Id):
|
||||
"""Delete/deactivate classroom"""
|
||||
classroom = Classroom.query.get_or_404(Id)
|
||||
|
||||
# Check for active reservations
|
||||
active_reservations = Reservation.query.filter(
|
||||
Reservation.classroom_id == Id,
|
||||
Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING])
|
||||
).count()
|
||||
|
||||
if active_reservations > 0:
|
||||
flash('Cannot delete classroom with active reservations.', 'error')
|
||||
return redirect(url_for('classrooms.view_classroom', Id=Id))
|
||||
def toggle_classroom(id):
|
||||
"""Toggle classroom active status"""
|
||||
classroom = Classroom.query.get_or_404(id)
|
||||
|
||||
try:
|
||||
classroom.is_active = False
|
||||
classroom.is_active = not classroom.is_active
|
||||
classroom.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
flash(f'Classroom {classroom.code} has been deactivated.', 'success')
|
||||
|
||||
status = 'activated' if classroom.is_active else 'deactivated'
|
||||
flash(f'Classroom {classroom.code_display} has been {status} successfully!', 'success')
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash('Error deactivating classroom.', 'error')
|
||||
flash(f'Error updating classroom status: {str(e)}', 'error')
|
||||
|
||||
return redirect(url_for('classrooms.list_classrooms'))
|
||||
|
||||
@classrooms_bp.route('/<int:classroom_id>/add_resource', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def add_resource(classroom_id):
|
||||
"""Add resource to classroom"""
|
||||
classroom = Classroom.query.get_or_404(classroom_id)
|
||||
form = ClassroomResourceForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
resource = ClassroomResource(
|
||||
classroom_id=classroom_id,
|
||||
resource_type=form.resource_type.data,
|
||||
brand=form.brand.data,
|
||||
model=form.model.data,
|
||||
specifications=form.specifications.data,
|
||||
quantity=form.quantity.data
|
||||
)
|
||||
|
||||
try:
|
||||
db.session.add(resource)
|
||||
db.session.commit()
|
||||
flash(f'{resource.resource_type} added to {classroom.code} successfully!', 'success')
|
||||
return redirect(url_for('classrooms.view_classroom', Id=classroom_id))
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
flash('Error adding resource.', 'error')
|
||||
|
||||
return render_template('classrooms/add_resource.html', form=form, classroom=classroom)
|
||||
return redirect(request.referrer or url_for('classrooms.list_classrooms'))
|
||||
|
||||
# API endpoints for AJAX requests
|
||||
@classrooms_bp.route('/api/available')
|
||||
@login_required
|
||||
def api_available_classrooms():
|
||||
"""API endpoint to get available classrooms"""
|
||||
def get_available_classrooms():
|
||||
"""Get available classrooms for a specific time slot"""
|
||||
start_time = request.args.get('start_time')
|
||||
end_time = request.args.get('end_time')
|
||||
min_capacity = request.args.get('min_capacity', 0, type=int)
|
||||
|
||||
if not start_time or not end_time:
|
||||
return jsonify({'error': 'start_time and end_time are required'}), 400
|
||||
return jsonify({'error': 'Start time and end time are required'}), 400
|
||||
|
||||
try:
|
||||
from datetime import datetime
|
||||
from app.models.reservation import Reservation, ReservationStatus
|
||||
|
||||
start_dt = datetime.fromisoformat(start_time)
|
||||
end_dt = datetime.fromisoformat(end_time)
|
||||
except ValueError:
|
||||
return jsonify({'error': 'Invalid datetime format'}), 400
|
||||
|
||||
# Find available classrooms (without conflicts)
|
||||
conflicting_classroom_ids = [
|
||||
r.classroom_id for r in Reservation.query.filter(
|
||||
Reservation.start_time < end_dt,
|
||||
Reservation.end_time > start_dt,
|
||||
Reservation.status == ReservationStatus.CONFIRMED
|
||||
).all()
|
||||
]
|
||||
|
||||
available_classrooms = Classroom.query.filter(
|
||||
Classroom.is_active == True,
|
||||
Classroom.capacity >= min_capacity,
|
||||
~Classroom.id.in_(conflicting_classroom_ids)
|
||||
).all()
|
||||
|
||||
return jsonify([classroom.to_dict() for classroom in available_classrooms])
|
||||
|
||||
# Find all classrooms
|
||||
all_classrooms = Classroom.query.filter_by(is_active=True).all()
|
||||
available_classrooms = []
|
||||
|
||||
for classroom in all_classrooms:
|
||||
# Check if classroom has conflicting reservations
|
||||
conflicts = Reservation.query.filter(
|
||||
Reservation.classroom_id == classroom.id,
|
||||
Reservation.status == ReservationStatus.CONFIRMED,
|
||||
Reservation.start_time < end_dt,
|
||||
Reservation.end_time > start_dt
|
||||
).all()
|
||||
|
||||
if not conflicts:
|
||||
available_classrooms.append({
|
||||
'id': classroom.id,
|
||||
'code': classroom.code,
|
||||
'building': classroom.building,
|
||||
'room_number': classroom.code,
|
||||
'floor': classroom.floor,
|
||||
'capacity': classroom.capacity,
|
||||
'code_display': classroom.code_display
|
||||
})
|
||||
|
||||
return jsonify({'classrooms': available_classrooms})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
+309
-256
@@ -1,277 +1,330 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, 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.models.subject import Commission
|
||||
from app.forms.reservation import ReservationForm
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import func, or_
|
||||
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__, url_prefix='/schedule')
|
||||
schedule_bp = Blueprint('schedule', __name__)
|
||||
|
||||
@schedule_bp.route('/')
|
||||
@schedule_bp.route('/add', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def calendar_view():
|
||||
"""Calendar view of reservations"""
|
||||
return render_template('schedule/calendar.html')
|
||||
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"""
|
||||
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')
|
||||
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:
|
||||
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)
|
||||
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"""
|
||||
today = datetime.utcnow().date()
|
||||
today_start = datetime.combine(today, datetime.min.time())
|
||||
today_end = datetime.combine(today, datetime.max.time())
|
||||
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)
|
||||
|
||||
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()
|
||||
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 render_template('schedule/today.html', reservations=reservations)
|
||||
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
|
||||
Reference in New Issue
Block a user