diff --git a/app/models/classroom.py b/app/models/classroom.py index d50aabe..1c4938c 100644 --- a/app/models/classroom.py +++ b/app/models/classroom.py @@ -55,6 +55,70 @@ class Classroom(db.Model): location += f" ({self.campus_location})" return location + @staticmethod + def find_best_classroom(max_students, start_time, end_time, exclude_reservation_id=None): + """Find the best suitable classroom for a commission based on capacity and availability""" + from app.models.reservation import Reservation, ReservationStatus + + # Get all active classrooms with sufficient capacity + suitable_classrooms = Classroom.query.filter( + Classroom.is_active == True, + Classroom.capacity >= max_students + ).order_by(Classroom.capacity.asc()).all() # Start with smallest suitable + + # Check availability for each classroom + available_classrooms = [] + for classroom in suitable_classrooms: + # Check for conflicts + conflicts = Reservation.find_conflicts( + classroom.id, start_time, end_time, exclude_reservation_id + ) + + if not conflicts: + # Calculate efficiency score (how well the capacity matches) + efficiency = classroom.capacity / max_students if max_students > 0 else float('inf') + available_classrooms.append({ + 'classroom': classroom, + 'efficiency': efficiency, + 'wasted_capacity': classroom.capacity - max_students + }) + + # Sort by efficiency (closest capacity match first) + available_classrooms.sort(key=lambda x: x['efficiency']) + + return available_classrooms + + @staticmethod + def get_availability_status(start_time, end_time, exclude_reservation_id=None): + """Get all classrooms classified as available or occupied for a time slot""" + from app.models.reservation import Reservation, ReservationStatus + + all_classrooms = Classroom.query.filter_by(is_active=True).all() + available = [] + occupied = [] + + for classroom in all_classrooms: + conflicts = Reservation.find_conflicts( + classroom.id, start_time, end_time, exclude_reservation_id + ) + + classroom_info = { + 'classroom': classroom, + 'conflicts': conflicts + } + + if conflicts: + occupied.append(classroom_info) + else: + available.append(classroom_info) + + return { + 'available': available, + 'occupied': occupied, + 'total_available': len(available), + 'total_occupied': len(occupied) + } + def __repr__(self): return f'' diff --git a/app/routes/schedule.py b/app/routes/schedule.py index 1ef9506..1376162 100644 --- a/app/routes/schedule.py +++ b/app/routes/schedule.py @@ -118,12 +118,80 @@ def calendar_view(): """Show calendar view""" return render_template('schedule/calendar.html', title='Calendar View') -@schedule_bp.route('/add') +@schedule_bp.route('/commissions') +@login_required +def commissions_view(): + """View commissions by subject for easy mapping""" + from app.models.subject import Subject, Commission + + # Get all active subjects with their commissions + subjects = Subject.query.filter_by(is_active=True).options( + db.joinedload(Subject.commissions).joinedload(Commission.teacher) + ).all() + + return render_template('schedule/commissions.html', title='Commissions Management', subjects=subjects) + +@schedule_bp.route('/add', methods=['GET', 'POST']) @login_required def add_reservation(): """Add new reservation""" + from app.models.classroom import Classroom + from app.models.subject import Commission + form = ReservationForm() - return render_template('schedule/create.html', form=form, title='New Reservation') + + # Get active classrooms for the dropdown + classrooms = Classroom.query.filter_by(is_active=True).all() + + # Get active commissions for the dropdown + commissions = Commission.query.filter_by(active=True).all() + + # Populate form choices + form.classroom_id.choices = [(c.id, f'{c.code_display} - {c.name} (Cap: {c.capacity})') for c in classrooms] + form.commission_id.choices = [(c.id, f'{c.get_full_code()} - {c.subject.name if c.subject else "No Subject"} (Students: {c.max_students})') for c in commissions] + + if request.method == 'POST': + if form.validate_on_submit(): + try: +# Check for existing reservations at the same time + from app.models.reservation import ReservationStatus + existing_reservation = Reservation.query.filter_by( + classroom_id=form.classroom_id.data, + status=ReservationStatus.CONFIRMED + ).filter( + Reservation.start_time < form.end_time.data, + Reservation.end_time > form.start_time.data + ).first() + + if existing_reservation: + flash('This classroom is already booked for the selected time slot.', 'danger') + return render_template('schedule/add.html', form=form, title='New Reservation', classroom_list=classrooms, commission_list=commissions) + + # Create new 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, + notes=form.notes.data, + status=ReservationStatus.PENDING, + expected_attendees=form.expected_attendees.data + ) + + db.session.add(reservation) + db.session.commit() + + flash('Reservation request submitted successfully! It will be reviewed by an administrator.', 'success') + return redirect(url_for('schedule.list_reservations')) + + except Exception as e: + db.session.rollback() + current_app.logger.error(f"Error creating reservation: {str(e)}") + flash(f'Error creating reservation: {str(e)}', 'danger') + + return render_template('schedule/add.html', form=form, title='New Reservation', classroom_list=classrooms, commission_list=commissions) @schedule_bp.route('/list') @login_required @@ -154,6 +222,94 @@ def confirm_reservation(id): return redirect(request.referrer) # API endpoints +@schedule_bp.route('/api/classroom-availability') +@login_required +def classroom_availability_api(): + """Get classroom availability for a specific date/time""" + try: + date_str = request.args.get('date') + start_time_str = request.args.get('start_time') + end_time_str = request.args.get('end_time') + max_students = request.args.get('max_students', type=int) + + if not all([date_str, start_time_str, end_time_str]): + return jsonify({'error': 'Missing required parameters: date, start_time, end_time'}), 400 + + # Parse datetime strings + from datetime import datetime + start_time = datetime.strptime(f"{date_str} {start_time_str}", "%Y-%m-%d %H:%M") + end_time = datetime.strptime(f"{date_str} {end_time_str}", "%Y-%m-%d %H:%M") + + # Get availability status + from app.models.classroom import Classroom + availability = Classroom.get_availability_status(start_time, end_time) + + # Get best classroom recommendation if max_students provided + best_recommendation = None + if max_students: + best_classrooms = Classroom.find_best_classroom(max_students, start_time, end_time) + if best_classrooms: + best_recommendation = best_classrooms[0] # Most efficient one + + # Format response + response = { + 'availability': { + 'available': [ + { + 'id': item['classroom'].id, + 'code': item['classroom'].code_display, + 'name': item['classroom'].name, + 'capacity': item['classroom'].capacity, + 'location': item['classroom'].get_full_location() + } + for item in availability['available'] + ], + 'occupied': [ + { + 'id': item['classroom'].id, + 'code': item['classroom'].code_display, + 'name': item['classroom'].name, + 'capacity': item['classroom'].capacity, + 'location': item['classroom'].get_full_location(), + 'conflicts': [ + { + 'start_time': conflict.start_time.strftime('%H:%M'), + 'end_time': conflict.end_time.strftime('%H:%M'), + 'commission': conflict.commission.get_full_code() if conflict.commission else 'Unknown' + } + for conflict in item['conflicts'] + ] + } + for item in availability['occupied'] + ], + 'stats': { + 'total_available': availability['total_available'], + 'total_occupied': availability['total_occupied'] + } + } + } + + if best_recommendation: + response['recommendation'] = { + 'classroom': { + 'id': best_recommendation['classroom'].id, + 'code': best_recommendation['classroom'].code_display, + 'name': best_recommendation['classroom'].name, + 'capacity': best_recommendation['classroom'].capacity, + 'location': best_recommendation['classroom'].get_full_location() + }, + 'efficiency': best_recommendation['efficiency'], + 'wasted_capacity': best_recommendation['wasted_capacity'], + 'reason': 'Best match based on capacity and availability' + } + + return jsonify(response) + + except ValueError as e: + return jsonify({'error': f'Invalid date/time format: {str(e)}'}), 400 + except Exception as e: + return jsonify({'error': str(e)}), 500 + @schedule_bp.route('/calendar-data') @login_required def calendar_data(): diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 75f177b..6edf965 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -107,9 +107,12 @@
- + Add Classroom + + Manage Commissions + New Reservation diff --git a/app/templates/schedule/add.html b/app/templates/schedule/add.html index d847d55..4498918 100644 --- a/app/templates/schedule/add.html +++ b/app/templates/schedule/add.html @@ -183,12 +183,13 @@ {% block extra_js %} +{% endblock %} \ No newline at end of file diff --git a/app/templates/schedule/create.html b/app/templates/schedule/create.html deleted file mode 100644 index d847d55..0000000 --- a/app/templates/schedule/create.html +++ /dev/null @@ -1,330 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Add Reservation - Edu-Space Admin{% endblock %} - -{% block content %} -
-
-
-
-

- New Reservation -

- - Back to Reservations - -
-
-
- -
-
-
-
-
- Reservation Details -
-
-
-
- {{ form.hidden_tag() }} - -
-
- {{ form.classroom_id.label(class="form-label") }} - {{ form.classroom_id(class="form-select", id="classroom-select") }} - {% if form.classroom_id.errors %} -
- {{ form.classroom_id.errors[0] }} -
- {% endif %} - Select a classroom for your reservation -
- -
- {{ form.commission_id.label(class="form-label") }} - {{ form.commission_id(class="form-select", id="commission-select") }} - {% if form.commission_id.errors %} -
- {{ form.commission_id.errors[0] }} -
- {% endif %} - Select the subject/commission -
-
- -
-
- {{ form.start_time.label(class="form-label") }} - {{ form.start_time(class="form-control", id="start-time") }} - {% if form.start_time.errors %} -
- {{ form.start_time.errors[0] }} -
- {% endif %} - When will the class start? -
- -
- {{ form.end_time.label(class="form-label") }} - {{ form.end_time(class="form-control", id="end-time") }} - {% if form.end_time.errors %} -
- {{ form.end_time.errors[0] }} -
- {% endif %} - When will the class end? -
-
- -
-
- {{ form.purpose.label(class="form-label") }} - {{ form.purpose(class="form-control", placeholder="Regular class, exam, study session, etc.") }} - {% if form.purpose.errors %} -
- {{ form.purpose.errors[0] }} -
- {% endif %} -
- -
- {{ form.expected_attendees.label(class="form-label") }} - {{ form.expected_attendees(class="form-control", placeholder="30") }} - {% if form.expected_attendees.errors %} -
- {{ form.expected_attendees.errors[0] }} -
- {% endif %} - Number of expected attendees -
-
- -
- {{ form.notes.label(class="form-label") }} - {{ form.notes(class="form-control", rows="3", placeholder="Any additional notes or special requirements...") }} - {% if form.notes.errors %} -
- {{ form.notes.errors[0] }} -
- {% endif %} -
- - -
-
- Reservation Summary -
-
-

- Selected Classroom: - Not selected -

-

- Duration: - Not calculated -

-

- Commission: - Not selected -

-
-
- -
- - Cancel - - {{ form.submit(class="btn btn-success") }} -
-
-
-
-
-
- - -
-
-
-
-
- Reservation Guidelines -
-
-
-
-
-
📅 Booking Rules
-
    -
  • Reservations can be made up to 3 months in advance
  • -
  • Cannot book in the past
  • -
  • Maximum duration: 8 hours per reservation
  • -
  • Cancellations must be 24 hours before start time
  • -
-
-
-
🚨 Important Notes
-
    -
  • Check classroom capacity before booking
  • -
  • Pending reservations require approval
  • -
  • Time slots must not overlap
  • -
  • System automatically detects conflicts
  • -
-
-
-
-
-
-
-
-{% endblock %} - -{% block extra_js %} - -{% endblock %} \ No newline at end of file