setup update 3

This commit is contained in:
Alejandro Vazquez
2026-03-26 20:14:35 -03:00
parent 9fb21c4a29
commit 192d1dd41c
12 changed files with 1928 additions and 595 deletions
+179 -161
View File
@@ -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
View File
@@ -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