247 lines
9.6 KiB
Python
247 lines
9.6 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, date
|
|
from sqlalchemy import func, or_
|
|
from sqlalchemy.orm import joinedload
|
|
|
|
schedule_bp = Blueprint('schedule', __name__, url_prefix='/schedule')
|
|
|
|
@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())
|
|
|
|
# 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()
|
|
|
|
# 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('/cancel/<int:id>', methods=['POST'])
|
|
@login_required
|
|
def cancel_reservation(id):
|
|
"""Cancel a reservation"""
|
|
try:
|
|
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)
|
|
|
|
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('/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
|
|
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.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])
|
|
).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': 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',
|
|
'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.options(
|
|
joinedload(Reservation.classroom),
|
|
joinedload(Reservation.commission)
|
|
).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() 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 if reservation.classroom else 'Unknown',
|
|
'purpose': reservation.purpose or 'No purpose',
|
|
'attendees': reservation.expected_attendees,
|
|
'status': reservation.status.value
|
|
}
|
|
})
|
|
|
|
return jsonify({'events': events})
|
|
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)}), 500 |