fix time table

This commit is contained in:
Alejandro Vazquez
2026-03-27 18:30:04 -03:00
parent 68e8d81617
commit 79f5da9bcd
8 changed files with 713 additions and 237 deletions
+129 -212
View File
@@ -1,227 +1,83 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
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 Commission
from app.models.subject import Subject, 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
from sqlalchemy import func, or_
from sqlalchemy.orm import joinedload
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 = Blueprint('schedule', __name__, url_prefix='/schedule')
@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)
today = datetime.utcnow().date()
today_start = datetime.combine(today, datetime.min.time())
today_end = datetime.combine(today, datetime.max.time())
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')
# Get reservations with eager loading
reservations = Reservation.query.options(
joinedload(Reservation.classroom),
joinedload(Reservation.commission),
joinedload(Reservation.user)
).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 redirect(request.referrer)
# Create time blocks for timeline view
time_blocks = {}
for reservation in reservations:
start_hour = reservation.start_time.hour
end_hour = reservation.end_time.hour
for hour in range(max(7, start_hour), min(22, end_hour + 1)):
if hour not in time_blocks:
time_blocks[hour] = []
if reservation not in time_blocks[hour]:
time_blocks[hour].append({
'reservation': reservation,
'classroom': reservation.classroom,
'commission': reservation.commission,
'user': reservation.user
})
# Calculate 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])
# Get total classrooms and calculate occupied ones
from app.models.classroom import Classroom
total_classrooms = Classroom.query.count()
occupied_classrooms = len(set(r.classroom_id for r in reservations if r.classroom_id))
stats = {
'total_reservations': total_reservations,
'confirmed': confirmed,
'pending': pending,
'occupied_classrooms': occupied_classrooms,
'total_classrooms': total_classrooms
}
return render_template('schedule/today.html',
reservations=reservations,
time_blocks=time_blocks,
stats=stats,
today=today)
@schedule_bp.route('/<int:id>/cancel', methods=['POST'])
@schedule_bp.route('/cancel/<int:id>', 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
reservation = Reservation.query.get_or_404(id)
if not reservation.can_be_cancelled():
flash('Reservations can only be cancelled at least 24 hours before start time.', 'error')
return redirect(request.referrer)
@@ -237,12 +93,66 @@ def cancel_reservation(id):
return redirect(request.referrer)
@schedule_bp.route('/reservation/<int:id>/view')
@login_required
def view_reservation(id):
"""View reservation details"""
try:
reservation = Reservation.query.options(
joinedload(Reservation.classroom),
joinedload(Reservation.commission),
joinedload(Reservation.user)
).get_or_404(id)
return render_template('schedule/view.html',
reservation=reservation,
title='Reservation Details')
except Exception as e:
flash(f'Error loading reservation details: {str(e)}', 'error')
return redirect(url_for('schedule.today_schedule'))
@schedule_bp.route('/calendar')
@login_required
def calendar_view():
"""Show calendar view"""
return render_template('schedule/calendar.html', title='Calendar View')
@schedule_bp.route('/add')
@login_required
def add_reservation():
"""Add new reservation"""
form = ReservationForm()
return render_template('schedule/create.html', form=form, title='New Reservation')
@schedule_bp.route('/list')
@login_required
def list_reservations():
"""List all reservations"""
return redirect(url_for('schedule.today_schedule'))
@schedule_bp.route('/reservation/<int:id>/confirm', methods=['POST'])
@login_required
def confirm_reservation(id):
"""Confirm a reservation"""
try:
reservation = Reservation.query.get_or_404(id)
if reservation.status != ReservationStatus.PENDING:
flash('Only pending reservations can be confirmed.', '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)
# API endpoints
@schedule_bp.route('/calendar-data')
@login_required
@@ -265,7 +175,10 @@ def calendar_data():
end_date = datetime.combine(next_month.replace(day=1), datetime.max.time())
# Get reservations in the date range
reservations = Reservation.query.filter(
reservations = Reservation.query.options(
joinedload(Reservation.classroom),
joinedload(Reservation.commission)
).filter(
Reservation.start_time >= start_date,
Reservation.start_time <= end_date,
Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING])
@@ -278,7 +191,7 @@ def calendar_data():
'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
'url': url_for('schedule.view_reservation', id=reservation.id) if reservation.id else None,
'className': f'fc-event-{reservation.status.value.lower()}',
'extendedProps': {
'classroom': reservation.classroom.code_display if reservation.classroom else 'Unknown',
@@ -303,7 +216,10 @@ def api_today_events():
today_start = datetime.combine(date.today(), datetime.min.time())
today_end = datetime.combine(date.today(), datetime.max.time())
reservations = Reservation.query.filter(
reservations = Reservation.query.options(
joinedload(Reservation.classroom),
joinedload(Reservation.commission)
).filter(
Reservation.start_time >= today_start,
Reservation.start_time <= today_end,
Reservation.status == ReservationStatus.CONFIRMED
@@ -313,12 +229,13 @@ def api_today_events():
for reservation in reservations:
events.append({
'id': reservation.id,
'title': f"{reservation.commission.get_full_code()} - {reservation.classroom.code_display}",
'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': url_for('schedule.view_reservation', id=reservation.id) if reservation.id else None,
'extendedProps': {
'classroom': reservation.classroom.code_display,
'purpose': reservation.purpose,
'classroom': reservation.classroom.code_display if reservation.classroom else 'Unknown',
'purpose': reservation.purpose or 'No purpose',
'attendees': reservation.expected_attendees,
'status': reservation.status.value
}