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
+2 -2
View File
@@ -43,8 +43,8 @@ def create_app():
app.register_blueprint(auth_bp)
app.register_blueprint(main_bp)
app.register_blueprint(classrooms_bp)
app.register_blueprint(schedule_bp)
app.register_blueprint(classrooms_bp, url_prefix='/classrooms')
app.register_blueprint(schedule_bp, url_prefix='/schedule')
# Create database tables
with app.app_context():
+25 -39
View File
@@ -1,58 +1,44 @@
from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, SelectField, TextAreaField, SubmitField
from wtforms.validators import DataRequired, NumberRange, Length, Optional
from wtforms import StringField, IntegerField, TextAreaField, BooleanField, SubmitField, SelectField
from wtforms.validators import DataRequired, Length, NumberRange, Optional
class ClassroomForm(FlaskForm):
code = StringField('Classroom Code', validators=[
DataRequired(),
Length(min=2, max=20, message='Code must be between 2 and 20 characters')
])
name = StringField('Classroom Name', validators=[
DataRequired(),
Length(max=100)
])
# campus_id = SelectField('Campus', coerce=int, validators=[DataRequired()])
building = StringField('Building', validators=[
DataRequired(),
Length(max=50)
Length(min=2, max=100)
])
room_number = StringField('Room Number', validators=[
DataRequired(),
Length(min=1, max=50)
])
floor = IntegerField('Floor', validators=[
DataRequired(),
NumberRange(min=1, max=10)
NumberRange(min=1, max=50)
])
capacity = IntegerField('Capacity', validators=[
DataRequired(),
NumberRange(min=1, max=200)
NumberRange(min=1, max=500)
])
description = TextAreaField('Description', validators=[Optional(), Length(max=500)])
campus_location = SelectField('Campus Location',
choices=[
('Main Campus', 'Main Campus'),
('North Campus', 'North Campus'),
('Science & Technology Campus', 'Science & Technology Campus'),
('Downtown Campus', 'Downtown Campus')
],
validators=[DataRequired()])
description = TextAreaField('Description', validators=[
Optional(),
Length(max=500)
])
is_active = BooleanField('Active', default=True)
submit = SubmitField('Save Classroom')
class ClassroomResourceForm(FlaskForm):
resource_type = SelectField('Resource Type',
choices=[
resource_type = SelectField('Resource Type', choices=[
('PROJECTOR', 'Projector'),
('WHITEBOARD', 'Whiteboard'),
('SMART_BOARD', 'Smart Board'),
('SCREEN', 'Screen'),
('AUDIO_SYSTEM', 'Audio System'),
('COMPUTERS', 'Computers'),
('LAB_EQUIPMENT', 'Lab Equipment'),
('AIR_CONDITIONING', 'Air Conditioning'),
('HEATING', 'Heating')
],
validators=[DataRequired()])
('COMPUTER', 'Computer'),
('SOUND_SYSTEM', 'Sound System'),
('MICROPHONE', 'Microphone'),
('AIR_CONDITIONER', 'Air Conditioner'),
('OTHER', 'Other')
], validators=[DataRequired()])
brand = StringField('Brand', validators=[Optional(), Length(max=100)])
model = StringField('Model', validators=[Optional(), Length(max=100)])
specifications = TextAreaField('Specifications', validators=[Optional(), Length(max=1000)])
quantity = IntegerField('Quantity', validators=[
DataRequired(),
NumberRange(min=1, max=50)
], default=1)
submit = SubmitField('Add Resource')
specifications = TextAreaField('Specifications', validators=[Optional()])
functional = BooleanField('Functional', default=True)
submit = SubmitField('Save Resource')
+28 -7
View File
@@ -1,19 +1,40 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, IntegerField, TextAreaField, SubmitField, DateTimeLocalField
from wtforms.validators import DataRequired, NumberRange, Length, Optional
from wtforms import SelectField, StringField, TextAreaField, IntegerField, SubmitField, DateTimeLocalField
from wtforms.validators import DataRequired, Length, NumberRange, Optional, ValidationError
from datetime import datetime, timedelta
class ReservationForm(FlaskForm):
classroom_id = SelectField('Classroom', coerce=int, validators=[DataRequired()])
commission_id = SelectField('Commission', coerce=int, validators=[DataRequired()])
commission_id = SelectField('Commission/Subject', coerce=int, validators=[DataRequired()])
start_time = DateTimeLocalField('Start Time', format='%Y-%m-%dT%H:%M', validators=[DataRequired()])
end_time = DateTimeLocalField('End Time', format='%Y-%m-%dT%H:%M', validators=[DataRequired()])
purpose = StringField('Purpose', validators=[
DataRequired(),
Length(max=500, message='Purpose must be less than 500 characters')
Length(min=3, max=500)
])
expected_attendees = IntegerField('Expected Attendees', validators=[
DataRequired(),
NumberRange(min=1, max=200, message='Number of attendees must be between 1 and 200')
NumberRange(min=1, max=500)
])
notes = TextAreaField('Notes', validators=[Optional(), Length(max=1000)])
submit = SubmitField('Save Reservation')
notes = TextAreaField('Notes', validators=[
Optional(),
Length(max=1000)
])
submit = SubmitField('Make Reservation')
def validate_end_time(self, field):
if field.data <= self.start_time.data:
raise ValidationError('End time must be after start time.')
# Check if duration is reasonable (max 8 hours)
if field.data > self.start_time.data + timedelta(hours=8):
raise ValidationError('Reservation cannot exceed 8 hours.')
def validate_start_time(self, field):
# Don't allow reservations in the past
if field.data < datetime.now():
raise ValidationError('Start time cannot be in the past.')
# Don't allow reservations too far in the future (max 3 months)
if field.data > datetime.now() + timedelta(days=90):
raise ValidationError('Reservations cannot be made more than 3 months in advance.')
+26 -13
View File
@@ -5,48 +5,58 @@ class Classroom(db.Model):
__tablename__ = 'classrooms'
id = db.Column(db.Integer, primary_key=True)
campus_id = db.Column(db.Integer, db.ForeignKey('campuses.id'), nullable=False)
# campus_id = db.Column(db.Integer, db.ForeignKey('campuses.id'), nullable=False)
building = db.Column(db.String(100), nullable=False)
room_number = db.Column(db.String(50), nullable=False)
code = db.Column(db.String(50), nullable=False) # Changed from room_number
name = db.Column(db.String(100), nullable=False)
floor = db.Column(db.Integer, nullable=False)
capacity = db.Column(db.Integer, nullable=False)
description = db.Column(db.Text)
campus_location = db.Column(db.String(100))
is_active = db.Column(db.Boolean, default=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Virtual field for code (combination of building and room_number)
# Virtual field for compatibility - combines building and code
@property
def code(self):
return f"{self.building}-{self.room_number}"
def room_number(self):
return self.code
# Relationships
reservations = db.relationship('Reservation', backref='classroom', lazy=True, cascade='all, delete-orphan')
@room_number.setter
def room_number(self, value):
self.code = value
# Resources relationship (教室资源)
resources = db.relationship('ClassroomResource', backref='classroom', lazy=True, cascade='all, delete-orphan')
# Override code property for consistency
@property
def code_display(self):
return f"{self.building}-{self.code}"
def to_dict(self):
"""Convert classroom to dictionary"""
return {
'id': self.id,
'code': self.code,
'code': self.code_display,
'building': self.building,
'name': self.name,
'floor': self.floor,
'capacity': self.capacity,
'description': self.description,
'campus_location': self.campus_location,
'is_active': self.is_active,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
'resources': [resource.to_dict() for resource in self.resources]
'resources': [resource.to_dict() for resource in self.resources] if hasattr(self, 'resources') else []
}
def get_full_location(self):
"""Get full classroom location"""
return f"{self.building} - Floor {self.floor}"
location = f"{self.building} - Floor {self.floor}"
if self.campus_location:
location += f" ({self.campus_location})"
return location
def __repr__(self):
return f'<Classroom {self.code}>'
return f'<Classroom {self.code_display}>'
class ClassroomResource(db.Model):
__tablename__ = 'classroom_resources'
@@ -62,6 +72,9 @@ class ClassroomResource(db.Model):
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
classroom = db.relationship('Classroom', backref=db.backref('resources', lazy=True, cascade='all, delete-orphan'))
def to_dict(self):
"""Convert resource to dictionary"""
return {
+13 -4
View File
@@ -10,7 +10,7 @@ class Subject(db.Model):
description = db.Column(db.Text)
department = db.Column(db.String(100), nullable=False)
credits = db.Column(db.Integer, nullable=False)
duration_in_minutes = db.Column(db.Integer, nullable=False)
# duration_in_minutes = db.Column(db.Integer, nullable=False)
is_active = db.Column(db.Boolean, default=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
@@ -27,7 +27,7 @@ class Subject(db.Model):
'description': self.description,
'department': self.department,
'credits': self.credits,
'duration_minutes': self.duration_in_minutes,
# 'duration_minutes': self.duration_in_minutes,
'is_active': self.is_active,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None
@@ -48,10 +48,19 @@ class Commission(db.Model):
max_students = db.Column(db.Integer, nullable=False)
current_students = db.Column(db.Integer, default=0)
schedule = db.Column(db.String(255))
is_active = db.Column(db.Boolean, default=True, nullable=False)
active = db.Column(db.Boolean, default=True) # Changed from is_active to active
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Virtual property for compatibility
@property
def is_active(self):
return self.active
@is_active.setter
def is_active(self, value):
self.active = value
# Relationships
reservations = db.relationship('Reservation', backref='commission', lazy=True, cascade='all, delete-orphan')
teacher = db.relationship('User', backref='teaching_commissions', foreign_keys=[teacher_id])
@@ -76,7 +85,7 @@ class Commission(db.Model):
'max_students': self.max_students,
'current_students': self.current_students,
'schedule': self.schedule,
'is_active': self.is_active,
'active': self.active,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
'subject': self.subject.to_dict() if self.subject else None
+174 -156
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,
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,
campus_location=form.campus_location.data
is_active=form.is_active.data
)
try:
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 edit_classroom(Id):
"""Edit classroom"""
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'))
form = ClassroomForm(obj=classroom)
if form.validate_on_submit():
def list_classrooms():
"""List all classrooms"""
try:
classroom.code = form.code.data
classroom.name = form.name.data
classroom.building = form.building.data
classroom.floor = form.floor.data
classroom.capacity = form.capacity.data
classroom.description = form.description.data
classroom.campus_location = form.campus_location.data
classroom.updated_at = datetime.utcnow()
# 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')
db.session.commit()
flash(f'Classroom {classroom.code} 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 loading classrooms: {str(e)}', 'error')
return render_template('classrooms/list.html', classrooms=None, title='Classrooms')
return render_template('classrooms/edit.html', form=form, classroom=classroom)
@classrooms_bp.route('/view/<int:Id>')
@classrooms_bp.route('/<int:id>')
@login_required
def view_classroom(Id):
def view_classroom(id):
"""View classroom details"""
classroom = Classroom.query.get_or_404(Id)
try:
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 upcoming reservations for this classroom
from app.models.reservation import Reservation, ReservationStatus
# 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])
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,
resources=resources,
upcoming_reservations=upcoming_reservations)
upcoming_reservations=upcoming_reservations,
title=f'Classroom {classroom.code_display}')
@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))
try:
classroom.is_active = False
classroom.updated_at = datetime.utcnow()
db.session.commit()
flash(f'Classroom {classroom.code} has been deactivated.', 'success')
except Exception as e:
db.session.rollback()
flash('Error deactivating classroom.', 'error')
flash(f'Error loading classroom: {str(e)}', 'error')
return redirect(url_for('classrooms.list_classrooms'))
@classrooms_bp.route('/<int:classroom_id>/add_resource', methods=['GET', 'POST'])
@classrooms_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def add_resource(classroom_id):
"""Add resource to classroom"""
classroom = Classroom.query.get_or_404(classroom_id)
form = ClassroomResourceForm()
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():
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)
# 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.is_active = form.is_active.data
classroom.updated_at = datetime.utcnow()
db.session.commit()
flash(f'{resource.resource_type} added to {classroom.code} 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 adding resource.', 'error')
flash(f'Error updating classroom: {str(e)}', 'error')
return render_template('classrooms/add_resource.html', form=form, classroom=classroom)
return render_template('classrooms/edit.html', form=form, classroom=classroom, title='Edit Classroom')
@classrooms_bp.route('/api/available')
@classrooms_bp.route('/<int:id>/toggle', methods=['POST'])
@login_required
def api_available_classrooms():
"""API endpoint to get available classrooms"""
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
def toggle_classroom(id):
"""Toggle classroom active status"""
classroom = Classroom.query.get_or_404(id)
try:
classroom.is_active = not classroom.is_active
classroom.updated_at = datetime.utcnow()
db.session.commit()
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(f'Error updating classroom status: {str(e)}', 'error')
return redirect(request.referrer or url_for('classrooms.list_classrooms'))
# API endpoints for AJAX requests
@classrooms_bp.route('/api/available')
@login_required
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')
if not start_time or not end_time:
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(
# 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,
Reservation.status == ReservationStatus.CONFIRMED
).all()
]
available_classrooms = Classroom.query.filter(
Classroom.is_active == True,
Classroom.capacity >= min_capacity,
~Classroom.id.in_(conflicting_classroom_ids)
Reservation.end_time > start_dt
).all()
return jsonify([classroom.to_dict() for classroom in available_classrooms])
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
+270 -217
View File
@@ -1,99 +1,52 @@
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.route('/')
@login_required
def calendar_view():
"""Calendar view of reservations"""
return render_template('schedule/calendar.html')
@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 = Blueprint('schedule', __name__)
@schedule_bp.route('/add', methods=['GET', 'POST'])
@login_required
def add_reservation():
"""Add new reservation"""
"""Add a 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]
# 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():
# Check for conflicts
conflicts = Reservation.find_conflicts(
try:
# Check for time conflicts
conflicting_reservations = 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)
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('Number of attendees exceeds classroom capacity.', 'error')
return render_template('schedule/add.html', form=form)
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,
@@ -102,176 +55,276 @@ def add_reservation():
end_time=form.end_time.data,
purpose=form.purpose.data,
expected_attendees=form.expected_attendees.data,
notes=form.notes.data,
status=ReservationStatus.PENDING
notes=form.notes.data
)
try:
db.session.add(reservation)
db.session.commit()
flash('Reservation created successfully!', 'success')
return redirect(url_for('schedule.view_reservation', Id=reservation.id))
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('Error creating reservation.', 'error')
flash(f'Error creating reservation: {str(e)}', 'error')
return render_template('schedule/add.html', form=form)
# Get classrooms for the template
classroom_list = Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all()
@schedule_bp.route('/view/<int:Id>')
return render_template('schedule/add.html', form=form, classroom_list=classroom_list, title='New Reservation')
@schedule_bp.route('/list')
@login_required
def view_reservation(Id):
"""View reservation details"""
reservation = Reservation.query.get_or_404(Id)
return render_template('schedule/view.html', reservation=reservation)
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')
@schedule_bp.route('/edit/<int:Id>', methods=['GET', 'POST'])
@login_required
def edit_reservation(Id):
"""Edit reservation"""
reservation = Reservation.query.get_or_404(Id)
# Build query
query = Reservation.query
# 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))
# Apply filters
if status_filter != 'all':
try:
query = query.filter_by(status=ReservationStatus[status_filter.upper()])
except KeyError:
pass
form = ReservationForm(obj=reservation)
if date_from:
try:
from_date = datetime.strptime(date_from, '%Y-%m-%d')
query = query.filter(Reservation.start_time >= from_date)
except ValueError:
pass
# 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]
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
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
# Order by start time descending
reservations = query.order_by(Reservation.start_time.desc()).paginate(
page=page, per_page=20, error_out=False
)
if conflicts:
flash('Classroom is already booked for this time slot.', 'error')
return render_template('schedule/edit.html', form=form, reservation=reservation)
return render_template('schedule/list.html',
reservations=reservations,
status_filter=status_filter,
date_from=date_from,
date_to=date_to,
title='Reservations')
# 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')
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)
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 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.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING])
Reservation.status == ReservationStatus.CONFIRMED
).order_by(Reservation.start_time).all()
return render_template('schedule/today.html', reservations=reservations)
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
+198
View File
@@ -0,0 +1,198 @@
{% extends "base.html" %}
{% block title %}Add Classroom - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">
<i class="bi bi-plus-circle text-primary"></i> Add Classroom
</h1>
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Classrooms
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0">
<h5 class="mb-0">
<i class="bi bi-door-open text-primary"></i> Classroom Information
</h5>
</div>
<div class="card-body">
<form method="POST" id="classroom-form" novalidate>
{{ form.hidden_tag() }}
<div class="row">
<div class="col-md-4 mb-3">
{{ form.building.label(class="form-label") }}
{{ form.building(class="form-control", placeholder="A") }}
{% if form.building.errors %}
<div class="text-danger small mt-1">
{{ form.building.errors[0] }}
</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
{{ form.room_number.label(class="form-label") }}
{{ form.room_number(class="form-control", placeholder="101") }}
{% if form.room_number.errors %}
<div class="text-danger small mt-1">
{{ form.room_number.errors[0] }}
</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
{{ form.floor.label(class="form-label") }}
{{ form.floor(class="form-control") }}
{% if form.floor.errors %}
<div class="text-danger small mt-1">
{{ form.floor.errors[0] }}
</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
{{ form.capacity.label(class="form-label") }}
{{ form.capacity(class="form-control", placeholder="30") }}
{% if form.capacity.errors %}
<div class="text-danger small mt-1">
{{ form.capacity.errors[0] }}
</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
{{ form.is_active.label(class="form-check-label") }}
<div class="form-check">
{{ form.is_active(class="form-check-input") }}
</div>
</div>
</div>
<div class="mb-4">
{{ form.description.label(class="form-label") }}
{{ form.description(class="form-control", rows="3", placeholder="Optional: Add any additional details about this classroom...") }}
{% if form.description.errors %}
<div class="text-danger small mt-1">
{{ form.description.errors[0] }}
</div>
{% endif %}
</div>
<!-- Preview Section -->
<div class="alert alert-info">
<h6 class="alert-heading">
<i class="bi bi-info-circle"></i> Preview
</h6>
<div id="classroom-preview">
<p class="mb-0">Fill in the form to see a preview of the classroom code:
<span class="fw-bold" id="preview-code">-</span>
</p>
<p class="mb-0">Full location:
<span class="fw-bold" id="preview-location">-</span>
</p>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> Cancel
</a>
{{ form.submit(class="btn btn-primary") }}
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Resources Section -->
<div class="row mt-4">
<div class="col-lg-8 mx-auto">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0">
<h5 class="mb-0">
<i class="bi bi-tools text-warning"></i> Resources (Optional)
</h5>
</div>
<div class="card-body">
<p class="text-muted">You can add classroom resources after creating the classroom.</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
function updatePreview() {
const building = $('#building').val();
const roomNumber = $('#room_number').val();
const floor = $('#floor').val();
if (building && roomNumber && floor) {
$('#preview-code').text(`${building}-${roomNumber}`);
$('#preview-location').text(`${building} - Floor ${floor}`);
} else {
$('#preview-code').text('-');
$('#preview-location').text('-');
}
}
// Update preview when inputs change
$('#building, #room_number, #floor').on('input', updatePreview);
// Initialize preview
updatePreview();
// Form validation
$('#classroom-form').on('submit', function(e) {
let isValid = true;
// Required fields validation
if (!$('#building').val().trim()) {
$('#building').addClass('is-invalid');
isValid = false;
} else {
$('#building').removeClass('is-invalid');
}
if (!$('#room_number').val().trim()) {
$('#room_number').addClass('is-invalid');
isValid = false;
} else {
$('#room_number').removeClass('is-invalid');
}
if (!$('#floor').val()) {
$('#floor').addClass('is-invalid');
isValid = false;
} else {
$('#floor').removeClass('is-invalid');
}
if (!$('#capacity').val() || $('#capacity').val() < 1) {
$('#capacity').addClass('is-invalid');
isValid = false;
} else {
$('#capacity').removeClass('is-invalid');
}
return isValid;
});
});
</script>
{% endblock %}
+108 -91
View File
@@ -3,124 +3,130 @@
{% block title %}Classrooms - Edu-Space Admin{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="container-fluid pt-5 mt-4">
<div class="row">
<div class="col-12">
<h1 class="h3 mb-0">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">
<i class="bi bi-door-open text-primary"></i> Classrooms
<small class="text-muted ms-2">Management</small>
</h1>
<div>
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add Classroom
</a>
</div>
</div>
</div>
</div>
<!-- Search and Filter -->
<div class="card border-0 shadow-sm mb-4">
<!-- Filters -->
<div class="row">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-4">
<input type="text" class="form-control" name="search"
placeholder="Search classrooms..." value="{{ search }}">
<input type="text" name="search" class="form-control"
placeholder="Search by building, room, description..."
value="{{ search }}">
</div>
<div class="col-md-3">
<select class="form-select" name="campus">
<option value="">All Campuses</option>
<option value="Main Campus" {% if campus == 'Main Campus' %}selected{% endif %}>Main Campus</option>
<option value="North Campus" {% if campus == 'North Campus' %}selected{% endif %}>North Campus</option>
<option value="Science & Technology Campus" {% if campus == 'Science & Technology Campus' %}selected{% endif %}>Science & Technology Campus</option>
<option value="Downtown Campus" {% if campus == 'Downtown Campus' %}selected{% endif %}>Downtown Campus</option>
</select>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="active_only"
id="activeOnly" {% if active_only %}checked{% endif %}>
<label class="form-check-label" for="activeOnly">
Show active classrooms only
</label>
</div>
</div>
<div class="col-md-3">
<button type="submit" class="btn btn-primary">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-search"></i> Search
</button>
<a href="{{ url_for('classrooms.list_classrooms') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-clockwise"></i> Reset
</a>
</div>
<div class="col-md-2 text-end">
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-success">
<i class="bi bi-plus-circle"></i> Add Classroom
<i class="bi bi-x-circle"></i> Clear
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Classrooms Grid -->
<div class="row">
{% if classrooms and classrooms.items %}
<div class="row mt-4">
{% for classroom in classrooms.items %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="col-lg-4 col-md-6 mb-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white border-0">
<div class="d-flex justify-content-between align-items-start">
<div>
<h5 class="card-title mb-1">{{ classroom.code }}</h5>
<p class="text-muted small mb-0">{{ classroom.name }}</p>
</div>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('classrooms.view_classroom', Id=classroom.id) }}">
<i class="bi bi-eye"></i> View Details
</a></li>
<li><a class="dropdown-item" href="{{ url_for('classrooms.edit_classroom', Id=classroom.id) }}">
<i class="bi bi-pencil"></i> Edit
</a></li>
<li><a class="dropdown-item text-danger" href="{{ url_for('classrooms.delete_classroom', Id=classroom.id) }}">
<i class="bi bi-trash"></i> Delete
</a></li>
</ul>
</div>
</div>
</div>
<div class="card-body">
<div class="row g-2">
<div class="col-6">
<div class="d-flex align-items-center">
<i class="bi bi-building text-primary me-2"></i>
<span class="small">{{ classroom.building }}</span>
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h5 class="card-title mb-1">
{{ classroom.code }}
</h5>
<p class="text-muted small mb-2">
<i class="bi bi-geo-alt"></i> {{ classroom.get_full_location() }}
</p>
</div>
<span class="badge {% if classroom.is_active %}bg-success{% else %}bg-secondary{% endif %}">
{% if classroom.is_active %}Active{% else %}Inactive{% endif %}
</span>
</div>
<div class="row small">
<div class="col-6">
<span class="text-muted">Capacity:</span>
<strong>{{ classroom.capacity }}</strong>
</div>
<div class="col-6">
<div class="d-flex align-items-center">
<i class="bi bi-layers text-info me-2"></i>
<span class="small">Floor {{ classroom.floor }}</span>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center">
<i class="bi bi-people text-success me-2"></i>
<span class="small">{{ classroom.capacity }} seats</span>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center">
<i class="bi bi-geo-alt text-warning me-2"></i>
<span class="small">{{ classroom.campus_location }}</span>
</div>
<span class="text-muted">Building:</span>
<strong>{{ classroom.building }}</strong>
</div>
</div>
{% if classroom.description %}
<div class="mt-3">
<p class="small text-muted">{{ classroom.description[:100] }}{% if classroom.description|length > 100 %}...{% endif %}</p>
</div>
<p class="card-text small text-muted mt-2">
{{ classroom.description[:100] }}{% if classroom.description|length > 100 %}...{% endif %}
</p>
{% endif %}
<div class="mt-3">
<span class="badge bg-success">
<i class="bi bi-check-circle"></i> Active
</span>
<!-- Resources indicator -->
<div class="mt-2">
<small class="text-muted">
<i class="bi bi-tools"></i>
{% if classroom.resources %}
{{ classroom.resources|length }} resources
{% else %}
No resources registered
{% endif %}
</small>
</div>
</div>
<div class="card-footer bg-white border-0">
<div class="d-grid">
<a href="{{ url_for('classrooms.view_classroom', Id=classroom.id) }}"
class="btn btn-outline-primary btn-sm">
<i class="bi bi-eye"></i> View Details
<div class="d-flex justify-content-between">
<div>
<a href="{{ url_for('classrooms.view_classroom', id=classroom.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> View
</a>
<a href="{{ url_for('classrooms.edit_classroom', id=classroom.id) }}"
class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i> Edit
</a>
</div>
<form method="POST" action="{{ url_for('classrooms.toggle_classroom', id=classroom.id) }}"
class="d-inline">
<button type="submit" class="btn btn-sm
{% if classroom.is_active %}btn-outline-warning{% else %}btn-outline-success{% endif %}">
{% if classroom.is_active %}
<i class="bi bi-pause-circle"></i> Deactivate
{% else %}
<i class="bi bi-play-circle"></i> Activate
{% endif %}
</button>
</form>
</div>
</div>
</div>
@@ -130,12 +136,12 @@
<!-- Pagination -->
{% if classrooms.pages > 1 %}
<nav aria-label="Classrooms pagination">
<nav aria-label="Classroom pagination">
<ul class="pagination justify-content-center">
{% if classrooms.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.prev_num, search=search, campus=campus) }}">
<i class="bi bi-chevron-left"></i>
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.prev_num, search=search, active_only=active_only) }}">
Previous
</a>
</li>
{% endif %}
@@ -144,7 +150,7 @@
{% if page_num %}
{% if page_num != classrooms.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=page_num, search=search, campus=campus) }}">
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=page_num, search=search, active_only=active_only) }}">
{{ page_num }}
</a>
</li>
@@ -155,15 +161,15 @@
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
<span class="page-link">…</span>
</li>
{% endif %}
{% endfor %}
{% if classrooms.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.next_num, search=search, campus=campus) }}">
<i class="bi bi-chevron-right"></i>
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.next_num, search=search, active_only=active_only) }}">
Next
</a>
</li>
{% endif %}
@@ -171,14 +177,25 @@
</nav>
{% endif %}
{% if not classrooms.items %}
<div class="text-center py-5">
<i class="bi bi-x-circle text-muted" style="font-size: 3rem;"></i>
<h4 class="text-muted mt-3">No classrooms found</h4>
<p class="text-muted">Try adjusting your search criteria or add a new classroom.</p>
{% else %}
<div class="row mt-4">
<div class="col-12">
<div class="text-center py-5">
<i class="bi bi-door-open text-muted" style="font-size: 4rem;"></i>
<h4 class="mt-3 text-muted">No classrooms found</h4>
<p class="text-muted">
{% if search %}
No classrooms match your search criteria.
{% else %}
No classrooms have been registered yet.
{% endif %}
</p>
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add First Classroom
</a>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
+330
View File
@@ -0,0 +1,330 @@
{% extends "base.html" %}
{% block title %}Add Reservation - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">
<i class="bi bi-calendar-plus text-success"></i> New Reservation
</h1>
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Back to Reservations
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0">
<h5 class="mb-0">
<i class="bi bi-calendar3 text-success"></i> Reservation Details
</h5>
</div>
<div class="card-body">
<form method="POST" id="reservation-form" novalidate>
{{ form.hidden_tag() }}
<div class="row">
<div class="col-md-6 mb-3">
{{ form.classroom_id.label(class="form-label") }}
{{ form.classroom_id(class="form-select", id="classroom-select") }}
{% if form.classroom_id.errors %}
<div class="text-danger small mt-1">
{{ form.classroom_id.errors[0] }}
</div>
{% endif %}
<small class="text-muted">Select a classroom for your reservation</small>
</div>
<div class="col-md-6 mb-3">
{{ form.commission_id.label(class="form-label") }}
{{ form.commission_id(class="form-select", id="commission-select") }}
{% if form.commission_id.errors %}
<div class="text-danger small mt-1">
{{ form.commission_id.errors[0] }}
</div>
{% endif %}
<small class="text-muted">Select the subject/commission</small>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
{{ form.start_time.label(class="form-label") }}
{{ form.start_time(class="form-control", id="start-time") }}
{% if form.start_time.errors %}
<div class="text-danger small mt-1">
{{ form.start_time.errors[0] }}
</div>
{% endif %}
<small class="text-muted">When will the class start?</small>
</div>
<div class="col-md-6 mb-3">
{{ form.end_time.label(class="form-label") }}
{{ form.end_time(class="form-control", id="end-time") }}
{% if form.end_time.errors %}
<div class="text-danger small mt-1">
{{ form.end_time.errors[0] }}
</div>
{% endif %}
<small class="text-muted">When will the class end?</small>
</div>
</div>
<div class="row">
<div class="col-md-8 mb-3">
{{ form.purpose.label(class="form-label") }}
{{ form.purpose(class="form-control", placeholder="Regular class, exam, study session, etc.") }}
{% if form.purpose.errors %}
<div class="text-danger small mt-1">
{{ form.purpose.errors[0] }}
</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
{{ form.expected_attendees.label(class="form-label") }}
{{ form.expected_attendees(class="form-control", placeholder="30") }}
{% if form.expected_attendees.errors %}
<div class="text-danger small mt-1">
{{ form.expected_attendees.errors[0] }}
</div>
{% endif %}
<small class="text-muted">Number of expected attendees</small>
</div>
</div>
<div class="mb-4">
{{ form.notes.label(class="form-label") }}
{{ form.notes(class="form-control", rows="3", placeholder="Any additional notes or special requirements...") }}
{% if form.notes.errors %}
<div class="text-danger small mt-1">
{{ form.notes.errors[0] }}
</div>
{% endif %}
</div>
<!-- Summary Section -->
<div class="alert alert-info" id="reservation-summary">
<h6 class="alert-heading">
<i class="bi bi-info-circle"></i> Reservation Summary
</h6>
<div id="summary-content">
<p class="mb-2">
<strong>Selected Classroom:</strong>
<span id="summary-classroom">Not selected</span>
</p>
<p class="mb-2">
<strong>Duration:</strong>
<span id="summary-duration">Not calculated</span>
</p>
<p class="mb-0">
<strong>Commission:</strong>
<span id="summary-commission">Not selected</span>
</p>
</div>
</div>
<div class="d-flex justify-content-between">
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> Cancel
</a>
{{ form.submit(class="btn btn-success") }}
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Instructions Card -->
<div class="row mt-4">
<div class="col-lg-8 mx-auto">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0">
<h5 class="mb-0">
<i class="bi bi-question-circle text-info"></i> Reservation Guidelines
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6 class="text-primary">📅 Booking Rules</h6>
<ul class="small">
<li>Reservations can be made up to 3 months in advance</li>
<li>Cannot book in the past</li>
<li>Maximum duration: 8 hours per reservation</li>
<li>Cancellations must be 24 hours before start time</li>
</ul>
</div>
<div class="col-md-6">
<h6 class="text-warning">🚨 Important Notes</h6>
<ul class="small">
<li>Check classroom capacity before booking</li>
<li>Pending reservations require approval</li>
<li>Time slots must not overlap</li>
<li>System automatically detects conflicts</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
// Classroom data for capacity checking
const classroomData = {};
{% for classroom in classroom_list %}
classroomData[{{ classroom.id }}] = {
capacity: {{ classroom.capacity }},
code: "{{ classroom.code }}"
};
{% endfor %}
function updateSummary() {
const classroomId = $('#classroom-select').val();
const commissionId = $('#commission-select').val();
const startTime = $('#start-time').val();
const endTime = $('#end-time').val();
// Update classroom summary
if (classroomId && classroomData[classroomId]) {
$('#summary-classroom').text(`${classroomData[classroomId].code} (Capacity: ${classroomData[classroomId].capacity})`);
} else {
$('#summary-classroom').text('Not selected');
}
// Update commission summary
if (commissionId) {
const commissionText = $('#commission-select option:selected').text();
$('#summary-commission').text(commissionText);
} else {
$('#summary-commission').text('Not selected');
}
// Calculate duration
if (startTime && endTime) {
const start = new Date(startTime);
const end = new Date(endTime);
if (end > start) {
const diffMs = end - start;
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffMins = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
let durationText = '';
if (diffHours > 0) {
durationText += `${diffHours} hour${diffHours > 1 ? 's' : ''}`;
}
if (diffMins > 0) {
if (durationText) durationText += ' ';
durationText += `${diffMins} minute${diffMins > 1 ? 's' : ''}`;
}
$('#summary-duration').text(durationText);
} else {
$('#summary-duration').text('Invalid time range');
}
} else {
$('#summary-duration').text('Not calculated');
}
}
// Check capacity vs expected attendees
function checkCapacity() {
const classroomId = $('#classroom-select').val();
const attendees = parseInt($('#expected_attendees').val()) || 0;
if (classroomId && classroomData[classroomId] && attendees > 0) {
const capacity = classroomData[classroomId].capacity;
if (attendees > capacity) {
$('#expected_attendees').addClass('is-invalid');
$('#expected_attendees').after(`
<div class="invalid-feedback">
Expected attendees (${attendees}) exceeds classroom capacity (${capacity})
</div>
`);
} else {
$('#expected_attendees').removeClass('is-invalid');
$('#expected_attendees').siblings('.invalid-feedback').remove();
}
}
}
// Update on change
$('#classroom-select, #commission-select, #start-time, #end-time, #expected_attendees').on('change', updateSummary);
$('#expected_attendees').on('input', checkCapacity);
// Initialize summary
updateSummary();
// Form validation
$('#reservation-form').on('submit', function(e) {
let isValid = true;
// Check required fields
if (!$('#classroom-select').val()) {
$('#classroom-select').addClass('is-invalid');
isValid = false;
} else {
$('#classroom-select').removeClass('is-invalid');
}
if (!$('#commission-select').val()) {
$('#commission-select').addClass('is-invalid');
isValid = false;
} else {
$('#commission-select').removeClass('is-invalid');
}
if (!$('#start-time').val()) {
$('#start-time').addClass('is-invalid');
isValid = false;
} else {
$('#start-time').removeClass('is-invalid');
}
if (!$('#end-time').val()) {
$('#end-time').addClass('is-invalid');
isValid = false;
} else {
$('#end-time').removeClass('is-invalid');
}
// Validate time range
const startTime = new Date($('#start-time').val());
const endTime = new Date($('#end-time').val());
if (endTime <= startTime) {
$('#end-time').addClass('is-invalid');
isValid = false;
}
// Check capacity
checkCapacity();
if ($('#expected_attendees').hasClass('is-invalid')) {
isValid = false;
}
return isValid;
});
// Set minimum datetime to current
const now = new Date();
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
$('#start-time, #end-time').attr('min', now.toISOString().slice(0, 16));
});
</script>
{% endblock %}
+291
View File
@@ -0,0 +1,291 @@
{% extends "base.html" %}
{% block title %}Reservations List - Edu-Space Admin{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<h1 class="h3 mb-0">
<i class="bi bi-calendar-list text-primary"></i> Reservations
<small class="text-muted ms-2">Manage & View All Reservations</small>
</h1>
<div class="d-flex gap-2">
<a href="{{ url_for('schedule.calendar_view') }}" class="btn btn-outline-primary">
<i class="bi bi-calendar3"></i> Calendar View
</a>
<a href="{{ url_for('schedule.today_schedule') }}" class="btn btn-outline-info">
<i class="bi bi-calendar-day"></i> Today
</a>
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add Reservation
</a>
</div>
</div>
</div>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-3">
<label class="form-label">Status</label>
<select name="status" class="form-select">
<option value="all" {% if status_filter == 'all' %}selected{% endif %}>All Status</option>
<option value="pending" {% if status_filter == 'pending' %}selected{% endif %}>Pending</option>
<option value="confirmed" {% if status_filter == 'confirmed' %}selected{% endif %}>Confirmed</option>
<option value="cancelled" {% if status_filter == 'cancelled' %}selected{% endif %}>Cancelled</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">From Date</label>
<input type="date" name="date_from" class="form-control" value="{{ date_from or '' }}">
</div>
<div class="col-md-3">
<label class="form-label">To Date</label>
<input type="date" name="date_to" class="form-control" value="{{ date_to or '' }}">
</div>
<div class="col-md-3">
<label class="form-label">&nbsp;</label>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-outline-primary">
<i class="bi bi-funnel"></i> Filter
</button>
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-circle"></i> Clear
</a>
</div>
</div>
</form>
</div>
</div>
<!-- Reservations List -->
{% if reservations and reservations.items %}
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th class="border-0">Classroom</th>
<th class="border-0">Commission</th>
<th class="border-0">Date & Time</th>
<th class="border-0">Purpose</th>
<th class="border-0">Attendees</th>
<th class="border-0">Status</th>
<th class="border-0">Actions</th>
</tr>
</thead>
<tbody>
{% for reservation in reservations.items %}
<tr>
<td>
<div class="d-flex align-items-center">
<div class="avatar avatar-sm bg-primary text-white rounded-circle d-flex align-items-center justify-content-center me-2">
<i class="bi bi-door-open"></i>
</div>
<div>
<div class="fw-bold">{{ reservation.classroom.code_display }}</div>
<small class="text-muted">Capacity: {{ reservation.classroom.capacity }}</small>
</div>
</div>
</td>
<td>
<div>
<div class="fw-bold">
{% if reservation.commission %}
{{ reservation.commission.get_full_code() or 'N/A' }}
{% else %}
No Commission
{% endif %}
</div>
<small class="text-muted">
{% if reservation.commission and reservation.commission.teacher_name %}
{{ reservation.commission.teacher_name }}
{% else %}
No Teacher
{% endif %}
</small>
</div>
</td>
<td>
<div>
<div class="fw-bold">{{ reservation.start_time.strftime('%b %d, %Y') }}</div>
<small class="text-muted">
{{ reservation.start_time.strftime('%I:%M %p') }} - {{ reservation.end_time.strftime('%I:%M %p') }}
</small>
</div>
</td>
<td>
<span class="text-truncate" style="max-width: 150px; display: inline-block;">
{{ reservation.purpose or 'No purpose specified' }}
</span>
</td>
<td>
<div class="d-flex align-items-center">
<i class="bi bi-people text-muted me-1"></i>
{{ reservation.expected_attendees }}
</div>
</td>
<td>
{% if reservation.status.value == 'confirmed' %}
<span class="badge bg-success">Confirmed</span>
{% elif reservation.status.value == 'pending' %}
<span class="badge bg-warning">Pending</span>
{% elif reservation.status.value == 'cancelled' %}
<span class="badge bg-danger">Cancelled</span>
{% else %}
<span class="badge bg-secondary">{{ reservation.status.value }}</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
{% if reservation.status.value == 'pending' %}
<form method="POST" action="{{ url_for('schedule.confirm_reservation', id=reservation.id) }}" style="display: inline;">
<button type="submit" class="btn btn-outline-success btn-sm" title="Confirm">
<i class="bi bi-check-circle"></i>
</button>
</form>
{% endif %}
{% if reservation.status.value in ['pending', 'confirmed'] %}
<form method="POST" action="{{ url_for('schedule.cancel_reservation', id=reservation.id) }}" style="display: inline;">
<button type="submit" class="btn btn-outline-danger btn-sm" title="Cancel">
<i class="bi bi-x-circle"></i>
</button>
</form>
{% endif %}
{% if reservation.notes %}
<button type="button" class="btn btn-outline-info btn-sm"
title="{{ reservation.notes }}" data-bs-toggle="tooltip">
<i class="bi bi-info-circle"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
{% if reservations.pages > 1 %}
<div class="card-footer bg-white border-0">
<div class="d-flex justify-content-between align-items-center">
<span class="text-muted small">
Showing {{ reservations.items|length }} of {{ reservations.total }} reservations
</span>
<nav>
<ul class="pagination pagination-sm mb-0">
{% if reservations.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('schedule.list_reservations', page=reservations.prev_num, status=status_filter, date_from=date_from, date_to=date_to) }}">
<i class="bi bi-chevron-left"></i>
</a>
</li>
{% endif %}
{% for page_num in reservations.iter_pages() %}
{% if page_num %}
{% if page_num != reservations.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('schedule.list_reservations', page=page_num, status=status_filter, date_from=date_from, date_to=date_to) }}">
{{ page_num }}
</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">…</span>
</li>
{% endif %}
{% endfor %}
{% if reservations.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('schedule.list_reservations', page=reservations.next_num, status=status_filter, date_from=date_from, date_to=date_to) }}">
<i class="bi bi-chevron-right"></i>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
</div>
{% endif %}
</div>
{% else %}
<div class="card border-0 shadow-sm">
<div class="card-body text-center py-5">
<div class="mb-4">
<i class="bi bi-calendar-x display-1 text-muted"></i>
</div>
<h5 class="text-muted">No reservations found</h5>
<p class="text-muted">
{% if status_filter != 'all' or date_from or date_to %}
No reservations match your current filters.
<a href="{{ url_for('schedule.list_reservations') }}">Clear filters</a> to see all reservations.
{% else %}
There are no reservations in the system yet.
<a href="{{ url_for('schedule.add_reservation') }}">Create your first reservation</a>.
{% endif %}
</p>
<div class="d-flex justify-content-center gap-2 mt-3">
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Add Reservation
</a>
<a href="{{ url_for('schedule.today_schedule') }}" class="btn btn-outline-info">
<i class="bi bi-calendar-day"></i> Today's Schedule
</a>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
// Initialize tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
// Form submission confirmation
document.querySelectorAll('form[action*="cancel"]').forEach(function(form) {
form.addEventListener('submit', function(e) {
if (!confirm('Are you sure you want to cancel this reservation?')) {
e.preventDefault();
}
});
});
document.querySelectorAll('form[action*="confirm"]').forEach(function(form) {
form.addEventListener('submit', function(e) {
if (!confirm('Are you sure you want to confirm this reservation?')) {
e.preventDefault();
}
});
});
</script>
{% endblock %}
+397
View File
@@ -0,0 +1,397 @@
{% extends "base.html" %}
{% block title %}Today's Schedule - Edu-Space Admin{% endblock %}
{% block content %}
<div class="container-fluid pt-5 mt-4">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">
<i class="bi bi-calendar-today text-info"></i> Today's Schedule
<small class="text-muted ms-2">{{ today.strftime('%B %d, %Y') }}</small>
</h1>
<div>
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-success">
<i class="bi bi-plus-circle"></i> New Reservation
</a>
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-primary ms-2">
<i class="bi bi-list"></i> All Reservations
</a>
</div>
</div>
</div>
</div>
<!-- Quick Stats -->
<div class="row mb-4">
<div class="col-md-3 mb-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">Total Reservations</h6>
<h2 class="mb-0">{{ stats.total_reservations }}</h2>
</div>
<div class="bg-info bg-opacity-10 rounded-circle p-3">
<i class="bi bi-calendar3 text-info fs-4"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">Confirmed</h6>
<h2 class="mb-0">{{ stats.confirmed }}</h2>
</div>
<div class="bg-success bg-opacity-10 rounded-circle p-3">
<i class="bi bi-check-circle text-success fs-4"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">Pending</h6>
<h2 class="mb-0">{{ stats.pending }}</h2>
</div>
<div class="bg-warning bg-opacity-10 rounded-circle p-3">
<i class="bi bi-clock text-warning fs-4"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 mb-3">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="text-muted mb-2">Active Classrooms</h6>
<h2 class="mb-0">{{ stats.occupied_classrooms }}/{{ stats.total_classrooms }}</h2>
</div>
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
<i class="bi bi-door-open text-primary fs-4"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Today's Schedule -->
{% if time_blocks %}
<div class="row">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="bi bi-clock-history text-info"></i> Timeline View
</h5>
<div>
<button class="btn btn-sm btn-outline-primary" id="refresh-btn">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
<button class="btn btn-sm btn-outline-secondary ms-1" id="toggle-view-btn">
<i class="bi bi-grid"></i> Grid View
</button>
</div>
</div>
</div>
<div class="card-body">
<div id="timeline-view">
{% for hour in range(7, 22) %}
<div class="row border-bottom py-3 align-items-start">
<div class="col-md-2 col-3">
<div class="text-center">
<strong>{{ "%02d:00"|format(hour) }}</strong>
<br>
<small class="text-muted">{{ hour+1 }}:00</small>
</div>
</div>
<div class="col-md-10 col-9">
{% if time_blocks[hour] %}
<div class="row">
{% for item in time_blocks[hour] %}
<div class="col-md-6 mb-3">
<div class="card border-0 shadow-sm h-100">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<h6 class="card-title mb-1">
{{ item.commission.get_full_code() }}
</h6>
<p class="text-muted small mb-2 mb-md-0">
<i class="bi bi-geo-alt"></i> {{ item.classroom.code }}
<br class="d-md-none">
<i class="bi bi-person ms-md-2"></i> {{ item.user.first_name }} {{ item.user.last_name }}
</p>
</div>
<span class="badge bg-{% if item.reservation.status.value == 'CONFIRMED' %}success{% elif item.reservation.status.value == 'PENDING' %}warning{% else %}secondary{% endif %}">
{{ item.reservation.status.value }}
</span>
</div>
<div class="row small">
<div class="col-md-6">
<i class="bi bi-clock"></i>
{{ item.reservation.start_time.strftime('%H:%M') }} - {{ item.reservation.end_time.strftime('%H:%M') }}
</div>
<div class="col-md-6">
<i class="bi bi-people"></i>
{{ item.reservation.expected_attendees }} attendees
</div>
</div>
{% if item.reservation.purpose %}
<p class="card-text small text-muted mt-2">
{{ item.reservation.purpose }}
</p>
{% endif %}
<div class="mt-2">
<a href="{{ url_for('schedule.view_reservation', id=item.reservation.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> Details
</a>
{% if item.reservation.status.value == 'PENDING' %}
<a href="{{ url_for('schedule.confirm_reservation', id=item.reservation.id) }}"
class="btn btn-sm btn-outline-success ms-1"
onclick="return confirm('Confirm this reservation?')">
<i class="bi bi-check-circle"></i> Confirm
</a>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center text-muted py-3">
<i class="bi bi-circle"></i> No reservations scheduled
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<!-- Grid View (hidden by default) -->
<div id="grid-view" style="display: none;">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Time</th>
<th>Classroom</th>
<th>Subject</th>
<th>Teacher</th>
<th>Purpose</th>
<th>Attendees</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for reservation in reservations %}
<tr>
<td>
{{ reservation.start_time.strftime('%H:%M') }} -
{{ reservation.end_time.strftime('%H:%M') }}
</td>
<td>
<i class="bi bi-geo-alt"></i>
{{ reservation.classroom.code }}
</td>
<td>{{ reservation.commission.get_full_code() }}</td>
<td>{{ reservation.user.first_name }} {{ reservation.user.last_name }}</td>
<td>{{ reservation.purpose }}</td>
<td>{{ reservation.expected_attendees }}</td>
<td>
<span class="badge bg-{% if reservation.status.value == 'CONFIRMED' %}success{% elif reservation.status.value == 'PENDING' %}warning{% else %}secondary{% endif %}">
{{ reservation.status.value }}
</span>
</td>
<td>
<a href="{{ url_for('schedule.view_reservation', id=reservation.id) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
{% if reservation.status.value == 'PENDING' %}
<a href="{{ url_for('schedule.confirm_reservation', id=reservation.id) }}"
class="btn btn-sm btn-outline-success ms-1"
onclick="return confirm('Confirm this reservation?')">
<i class="bi bi-check-circle"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Daily Summary -->
<div class="row mt-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-0">
<h5 class="mb-0">
<i class="bi bi-graph-up text-warning"></i> Daily Analytics
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="text-center">
<div class="mb-2">
<i class="bi bi-door-open-fill text-success" style="font-size: 2rem;"></i>
</div>
<h6>Most Active Hours</h6>
<!-- Calculate peak hours from data -->
{% set peak_hours = [] %}
{% for item in reservations %}
{% set slot = item.reservation.start_time.hour %}
{% if slot not in peak_hours %}
{% set _ = peak_hours.append(slot) %}
{% endif %}
{% endfor %}
<p class="text-muted">
{% if peak_hours %}
{% for hour in peak_hours[:3]|sort %}
{{ "%02d:00"|format(hour) }}{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
No data available
{% endif %}
</p>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<div class="mb-2">
<i class="bi bi-building text-info" style="font-size: 2rem;"></i>
</div>
<h6>Busiest Classrooms</h6>
{% set usage_counter = {} %}
{% for item in reservations %}
{% set code = item.classroom.code %}
{% if code not in usage_counter %}
{% set _ = usage_counter.update({code: 1}) %}
{% else %}
{% set _ = usage_counter.update({code: usage_counter[code] + 1}) %}
{% endif %}
{% endfor %}
<p class="text-muted">
{% if usage_counter %}
{% for code, count in usage_counter.items()|sort(attribute='1', reverse=true)|slice(3) %}
{{ code }} ({{ count }}){% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
No data available
{% endif %}
</p>
</div>
</div>
<div class="col-md-4">
<div class="text-center">
<div class="mb-2">
<i class="bi bi-clock-fill text-primary" style="font-size: 2rem;"></i>
</div>
<h6>Total Hours Booked</h6>
{% set total_minutes = 0 %}
{% for item in reservations %}
{% set duration = (item.reservation.end_time - item.reservation.start_time).total_seconds() / 60 %}
{% set _ = total_minutes.__iadd__(duration) %}
{% endfor %}
<p class="text-muted">
{{ (total_minutes / 60)|round(1) }} hours total
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="row">
<div class="col-12">
<div class="text-center py-5">
<i class="bi bi-calendar-x text-muted" style="font-size: 4rem;"></i>
<h4 class="mt-3 text-muted">No reservations scheduled for today</h4>
<p class="text-muted">
There are no classroom reservations for {{ today.strftime('%B %d, %Y') }}.
</p>
<div class="mt-3">
<a href="{{ url_for('schedule.add_reservation') }}" class="btn btn-success">
<i class="bi bi-plus-circle"></i> Schedule First Class
</a>
<a href="{{ url_for('schedule.list_reservations') }}" class="btn btn-outline-primary ms-2">
<i class="bi bi-calendar-week"></i>View Week Schedule
</a>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
<script>
$(document).ready(function() {
// Toggle between timeline and grid view
$('#toggle-view-btn').on('click', function() {
const timelineView = $('#timeline-view');
const gridView = $('#grid-view');
const btn = $(this);
if (timelineView.is(':visible')) {
timelineView.hide();
gridView.show();
btn.html('<i class="bi bi-list"></i> Timeline View');
} else {
timelineView.show();
gridView.hide();
btn.html('<i class="bi bi-grid"></i> Grid View');
}
});
// Refresh the page
$('#refresh-btn').on('click', function() {
location.reload();
});
// Auto-refresh every 5 minutes
setInterval(function() {
location.reload();
}, 300000); // 5 minutes = 300000 milliseconds
// Highlight current time
const currentHour = new Date().getHours();
if (currentHour >= 7 && currentHour < 22) {
const timeBlockIndex = currentHour - 7;
$('.row.border-bottom').eq(timeBlockIndex).addClass('bg-primary bg-opacity-10');
}
});
</script>
{% endblock %}