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(auth_bp)
app.register_blueprint(main_bp) app.register_blueprint(main_bp)
app.register_blueprint(classrooms_bp) app.register_blueprint(classrooms_bp, url_prefix='/classrooms')
app.register_blueprint(schedule_bp) app.register_blueprint(schedule_bp, url_prefix='/schedule')
# Create database tables # Create database tables
with app.app_context(): with app.app_context():
+27 -41
View File
@@ -1,58 +1,44 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, SelectField, TextAreaField, SubmitField from wtforms import StringField, IntegerField, TextAreaField, BooleanField, SubmitField, SelectField
from wtforms.validators import DataRequired, NumberRange, Length, Optional from wtforms.validators import DataRequired, Length, NumberRange, Optional
class ClassroomForm(FlaskForm): class ClassroomForm(FlaskForm):
code = StringField('Classroom Code', validators=[ # campus_id = SelectField('Campus', coerce=int, validators=[DataRequired()])
DataRequired(),
Length(min=2, max=20, message='Code must be between 2 and 20 characters')
])
name = StringField('Classroom Name', validators=[
DataRequired(),
Length(max=100)
])
building = StringField('Building', validators=[ building = StringField('Building', validators=[
DataRequired(), 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=[ floor = IntegerField('Floor', validators=[
DataRequired(), DataRequired(),
NumberRange(min=1, max=10) NumberRange(min=1, max=50)
]) ])
capacity = IntegerField('Capacity', validators=[ capacity = IntegerField('Capacity', validators=[
DataRequired(), DataRequired(),
NumberRange(min=1, max=200) NumberRange(min=1, max=500)
]) ])
description = TextAreaField('Description', validators=[Optional(), Length(max=500)]) description = TextAreaField('Description', validators=[
campus_location = SelectField('Campus Location', Optional(),
choices=[ Length(max=500)
('Main Campus', 'Main Campus'), ])
('North Campus', 'North Campus'), is_active = BooleanField('Active', default=True)
('Science & Technology Campus', 'Science & Technology Campus'),
('Downtown Campus', 'Downtown Campus')
],
validators=[DataRequired()])
submit = SubmitField('Save Classroom') submit = SubmitField('Save Classroom')
class ClassroomResourceForm(FlaskForm): class ClassroomResourceForm(FlaskForm):
resource_type = SelectField('Resource Type', resource_type = SelectField('Resource Type', choices=[
choices=[ ('PROJECTOR', 'Projector'),
('PROJECTOR', 'Projector'), ('WHITEBOARD', 'Whiteboard'),
('WHITEBOARD', 'Whiteboard'), ('COMPUTER', 'Computer'),
('SMART_BOARD', 'Smart Board'), ('SOUND_SYSTEM', 'Sound System'),
('SCREEN', 'Screen'), ('MICROPHONE', 'Microphone'),
('AUDIO_SYSTEM', 'Audio System'), ('AIR_CONDITIONER', 'Air Conditioner'),
('COMPUTERS', 'Computers'), ('OTHER', 'Other')
('LAB_EQUIPMENT', 'Lab Equipment'), ], validators=[DataRequired()])
('AIR_CONDITIONING', 'Air Conditioning'),
('HEATING', 'Heating')
],
validators=[DataRequired()])
brand = StringField('Brand', validators=[Optional(), Length(max=100)]) brand = StringField('Brand', validators=[Optional(), Length(max=100)])
model = StringField('Model', validators=[Optional(), Length(max=100)]) model = StringField('Model', validators=[Optional(), Length(max=100)])
specifications = TextAreaField('Specifications', validators=[Optional(), Length(max=1000)]) specifications = TextAreaField('Specifications', validators=[Optional()])
quantity = IntegerField('Quantity', validators=[ functional = BooleanField('Functional', default=True)
DataRequired(), submit = SubmitField('Save Resource')
NumberRange(min=1, max=50)
], default=1)
submit = SubmitField('Add Resource')
+28 -7
View File
@@ -1,19 +1,40 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, IntegerField, TextAreaField, SubmitField, DateTimeLocalField from wtforms import SelectField, StringField, TextAreaField, IntegerField, SubmitField, DateTimeLocalField
from wtforms.validators import DataRequired, NumberRange, Length, Optional from wtforms.validators import DataRequired, Length, NumberRange, Optional, ValidationError
from datetime import datetime, timedelta
class ReservationForm(FlaskForm): class ReservationForm(FlaskForm):
classroom_id = SelectField('Classroom', coerce=int, validators=[DataRequired()]) 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()]) 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()]) end_time = DateTimeLocalField('End Time', format='%Y-%m-%dT%H:%M', validators=[DataRequired()])
purpose = StringField('Purpose', validators=[ purpose = StringField('Purpose', validators=[
DataRequired(), DataRequired(),
Length(max=500, message='Purpose must be less than 500 characters') Length(min=3, max=500)
]) ])
expected_attendees = IntegerField('Expected Attendees', validators=[ expected_attendees = IntegerField('Expected Attendees', validators=[
DataRequired(), 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)]) notes = TextAreaField('Notes', validators=[
submit = SubmitField('Save Reservation') 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' __tablename__ = 'classrooms'
id = db.Column(db.Integer, primary_key=True) 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) 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) floor = db.Column(db.Integer, nullable=False)
capacity = db.Column(db.Integer, nullable=False) capacity = db.Column(db.Integer, nullable=False)
description = db.Column(db.Text) description = db.Column(db.Text)
campus_location = db.Column(db.String(100))
is_active = db.Column(db.Boolean, default=True, nullable=False) is_active = db.Column(db.Boolean, default=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=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 @property
def code(self): def room_number(self):
return f"{self.building}-{self.room_number}" return self.code
# Relationships @room_number.setter
reservations = db.relationship('Reservation', backref='classroom', lazy=True, cascade='all, delete-orphan') def room_number(self, value):
self.code = value
# Resources relationship (教室资源) # Override code property for consistency
resources = db.relationship('ClassroomResource', backref='classroom', lazy=True, cascade='all, delete-orphan') @property
def code_display(self):
return f"{self.building}-{self.code}"
def to_dict(self): def to_dict(self):
"""Convert classroom to dictionary""" """Convert classroom to dictionary"""
return { return {
'id': self.id, 'id': self.id,
'code': self.code, 'code': self.code_display,
'building': self.building, 'building': self.building,
'name': self.name,
'floor': self.floor, 'floor': self.floor,
'capacity': self.capacity, 'capacity': self.capacity,
'description': self.description, 'description': self.description,
'campus_location': self.campus_location,
'is_active': self.is_active, 'is_active': self.is_active,
'created_at': self.created_at.isoformat() if self.created_at else None, 'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_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): def get_full_location(self):
"""Get full classroom location""" """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): def __repr__(self):
return f'<Classroom {self.code}>' return f'<Classroom {self.code_display}>'
class ClassroomResource(db.Model): class ClassroomResource(db.Model):
__tablename__ = 'classroom_resources' __tablename__ = 'classroom_resources'
@@ -62,6 +72,9 @@ class ClassroomResource(db.Model):
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=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): def to_dict(self):
"""Convert resource to dictionary""" """Convert resource to dictionary"""
return { return {
+13 -4
View File
@@ -10,7 +10,7 @@ class Subject(db.Model):
description = db.Column(db.Text) description = db.Column(db.Text)
department = db.Column(db.String(100), nullable=False) department = db.Column(db.String(100), nullable=False)
credits = db.Column(db.Integer, 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) is_active = db.Column(db.Boolean, default=True, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=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, 'description': self.description,
'department': self.department, 'department': self.department,
'credits': self.credits, 'credits': self.credits,
'duration_minutes': self.duration_in_minutes, # 'duration_minutes': self.duration_in_minutes,
'is_active': self.is_active, 'is_active': self.is_active,
'created_at': self.created_at.isoformat() if self.created_at else None, 'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_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) max_students = db.Column(db.Integer, nullable=False)
current_students = db.Column(db.Integer, default=0) current_students = db.Column(db.Integer, default=0)
schedule = db.Column(db.String(255)) 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) created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=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 # Relationships
reservations = db.relationship('Reservation', backref='commission', lazy=True, cascade='all, delete-orphan') reservations = db.relationship('Reservation', backref='commission', lazy=True, cascade='all, delete-orphan')
teacher = db.relationship('User', backref='teaching_commissions', foreign_keys=[teacher_id]) teacher = db.relationship('User', backref='teaching_commissions', foreign_keys=[teacher_id])
@@ -76,7 +85,7 @@ class Commission(db.Model):
'max_students': self.max_students, 'max_students': self.max_students,
'current_students': self.current_students, 'current_students': self.current_students,
'schedule': self.schedule, 'schedule': self.schedule,
'is_active': self.is_active, 'active': self.active,
'created_at': self.created_at.isoformat() if self.created_at else None, 'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_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 '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 import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required 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 import db
from app.models.classroom import Classroom, ClassroomResource from sqlalchemy import text
from app.models.reservation import Reservation, ReservationStatus
from app.forms.classroom import ClassroomForm, ClassroomResourceForm
from datetime import datetime
classrooms_bp = Blueprint('classrooms', __name__, url_prefix='/classrooms') classrooms_bp = Blueprint('classrooms', __name__)
@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.route('/add', methods=['GET', 'POST']) @classrooms_bp.route('/add', methods=['GET', 'POST'])
@login_required @login_required
def add_classroom(): def add_classroom():
"""Add new classroom""" """Add a new classroom"""
form = ClassroomForm() form = ClassroomForm()
if form.validate_on_submit(): # TODO: Update when campus model is implemented
classroom = Classroom( # form.campus_id.choices = [(1, 'Main Campus')]
code=form.code.data,
name=form.name.data,
building=form.building.data,
floor=form.floor.data,
capacity=form.capacity.data,
description=form.description.data,
campus_location=form.campus_location.data
)
if form.validate_on_submit():
try: try:
existing_classroom = Classroom.query.filter_by(
building=form.building.data,
code=form.room_number.data,
floor=form.floor.data
).first()
if existing_classroom:
flash('Classroom with this building, room number, and floor already exists.', 'error')
return render_template('classrooms/add.html', form=form, title='Add Classroom')
classroom = Classroom(
# campus_id=form.campus_id.data,
building=form.building.data,
code=form.room_number.data,
name=f"{form.building.data}-{form.room_number.data}",
floor=form.floor.data,
capacity=form.capacity.data,
description=form.description.data,
is_active=form.is_active.data
)
db.session.add(classroom) db.session.add(classroom)
db.session.commit() 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')) return redirect(url_for('classrooms.list_classrooms'))
except Exception as e: except Exception as e:
db.session.rollback() 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 @login_required
def edit_classroom(Id): def list_classrooms():
"""Edit classroom""" """List all classrooms"""
classroom = Classroom.query.get_or_404(Id) try:
# Get filters
page = request.args.get('page', 1, type=int)
active_only = request.args.get('active_only', 'true').lower() == 'true'
search = request.args.get('search', '', type=str)
if not classroom.is_active: # Build query
flash('This classroom has been deactivated.', 'warning') query = Classroom.query
if active_only:
query = query.filter_by(is_active=True)
if search:
query = query.filter(
db.or_(
Classroom.building.ilike(f'%{search}%'),
Classroom.code.ilike(f'%{search}%'),
Classroom.description.ilike(f'%{search}%')
)
)
# Paginate results
classrooms = query.order_by(Classroom.building, Classroom.floor, Classroom.code).paginate(
page=page, per_page=12, error_out=False
)
return render_template('classrooms/list.html',
classrooms=classrooms,
active_only=active_only,
search=search,
title='Classrooms')
except Exception as e:
flash(f'Error loading classrooms: {str(e)}', 'error')
return render_template('classrooms/list.html', classrooms=None, title='Classrooms')
@classrooms_bp.route('/<int:id>')
@login_required
def view_classroom(id):
"""View classroom details"""
try:
classroom = Classroom.query.get_or_404(id)
# Get upcoming reservations for this classroom
from app.models.reservation import Reservation, ReservationStatus
upcoming_reservations = Reservation.query.filter(
Reservation.classroom_id == id,
Reservation.start_time > datetime.utcnow(),
Reservation.status == ReservationStatus.CONFIRMED
).order_by(Reservation.start_time).limit(10).all()
return render_template('classrooms/view.html',
classroom=classroom,
upcoming_reservations=upcoming_reservations,
title=f'Classroom {classroom.code_display}')
except Exception as e:
flash(f'Error loading classroom: {str(e)}', 'error')
return redirect(url_for('classrooms.list_classrooms')) return redirect(url_for('classrooms.list_classrooms'))
@classrooms_bp.route('/<int:id>/edit', methods=['GET', 'POST'])
@login_required
def edit_classroom(id):
"""Edit classroom"""
classroom = Classroom.query.get_or_404(id)
form = ClassroomForm(obj=classroom) form = ClassroomForm(obj=classroom)
# TODO: Update when campus model is implemented
# form.campus_id.choices = [(1, 'Main Campus')]
if form.validate_on_submit(): if form.validate_on_submit():
try: try:
classroom.code = form.code.data # Check for duplicates (excluding current classroom)
classroom.name = form.name.data 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.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.floor = form.floor.data
classroom.capacity = form.capacity.data classroom.capacity = form.capacity.data
classroom.description = form.description.data classroom.description = form.description.data
classroom.campus_location = form.campus_location.data classroom.is_active = form.is_active.data
classroom.updated_at = datetime.utcnow() classroom.updated_at = datetime.utcnow()
db.session.commit() db.session.commit()
flash(f'Classroom {classroom.code} updated successfully!', 'success')
return redirect(url_for('classrooms.view_classroom', Id=classroom.id)) flash(f'Classroom {classroom.code_display} has been updated successfully!', 'success')
return redirect(url_for('classrooms.view_classroom', id=classroom.id))
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
flash('Error updating classroom.', 'error') flash(f'Error updating classroom: {str(e)}', 'error')
return render_template('classrooms/edit.html', form=form, classroom=classroom) return render_template('classrooms/edit.html', form=form, classroom=classroom, title='Edit Classroom')
@classrooms_bp.route('/view/<int:Id>') @classrooms_bp.route('/<int:id>/toggle', methods=['POST'])
@login_required @login_required
def view_classroom(Id): def toggle_classroom(id):
"""View classroom details""" """Toggle classroom active status"""
classroom = Classroom.query.get_or_404(Id) classroom = Classroom.query.get_or_404(id)
if not classroom.is_active:
flash('This classroom has been deactivated.', 'warning')
return redirect(url_for('classrooms.list_classrooms'))
# Get classroom resources
resources = ClassroomResource.query.filter_by(classroom_id=Id).all()
# Get upcoming reservations
upcoming_reservations = Reservation.query.filter(
Reservation.classroom_id == Id,
Reservation.start_time >= datetime.utcnow(),
Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING])
).order_by(Reservation.start_time).limit(10).all()
return render_template('classrooms/view.html',
classroom=classroom,
resources=resources,
upcoming_reservations=upcoming_reservations)
@classrooms_bp.route('/delete/<int:Id>')
@login_required
def delete_classroom(Id):
"""Delete/deactivate classroom"""
classroom = Classroom.query.get_or_404(Id)
# Check for active reservations
active_reservations = Reservation.query.filter(
Reservation.classroom_id == Id,
Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING])
).count()
if active_reservations > 0:
flash('Cannot delete classroom with active reservations.', 'error')
return redirect(url_for('classrooms.view_classroom', Id=Id))
try: try:
classroom.is_active = False classroom.is_active = not classroom.is_active
classroom.updated_at = datetime.utcnow() classroom.updated_at = datetime.utcnow()
db.session.commit() db.session.commit()
flash(f'Classroom {classroom.code} has been deactivated.', 'success')
status = 'activated' if classroom.is_active else 'deactivated'
flash(f'Classroom {classroom.code_display} has been {status} successfully!', 'success')
except Exception as e: except Exception as e:
db.session.rollback() db.session.rollback()
flash('Error deactivating classroom.', 'error') flash(f'Error updating classroom status: {str(e)}', 'error')
return redirect(url_for('classrooms.list_classrooms')) return redirect(request.referrer or url_for('classrooms.list_classrooms'))
@classrooms_bp.route('/<int:classroom_id>/add_resource', methods=['GET', 'POST'])
@login_required
def add_resource(classroom_id):
"""Add resource to classroom"""
classroom = Classroom.query.get_or_404(classroom_id)
form = ClassroomResourceForm()
if form.validate_on_submit():
resource = ClassroomResource(
classroom_id=classroom_id,
resource_type=form.resource_type.data,
brand=form.brand.data,
model=form.model.data,
specifications=form.specifications.data,
quantity=form.quantity.data
)
try:
db.session.add(resource)
db.session.commit()
flash(f'{resource.resource_type} added to {classroom.code} successfully!', 'success')
return redirect(url_for('classrooms.view_classroom', Id=classroom_id))
except Exception as e:
db.session.rollback()
flash('Error adding resource.', 'error')
return render_template('classrooms/add_resource.html', form=form, classroom=classroom)
# API endpoints for AJAX requests
@classrooms_bp.route('/api/available') @classrooms_bp.route('/api/available')
@login_required @login_required
def api_available_classrooms(): def get_available_classrooms():
"""API endpoint to get available classrooms""" """Get available classrooms for a specific time slot"""
start_time = request.args.get('start_time') start_time = request.args.get('start_time')
end_time = request.args.get('end_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: if not start_time or not end_time:
return jsonify({'error': 'start_time and end_time are required'}), 400 return jsonify({'error': 'Start time and end time are required'}), 400
try: try:
from datetime import datetime
from app.models.reservation import Reservation, ReservationStatus
start_dt = datetime.fromisoformat(start_time) start_dt = datetime.fromisoformat(start_time)
end_dt = datetime.fromisoformat(end_time) end_dt = datetime.fromisoformat(end_time)
except ValueError:
return jsonify({'error': 'Invalid datetime format'}), 400
# Find available classrooms (without conflicts) # Find all classrooms
conflicting_classroom_ids = [ all_classrooms = Classroom.query.filter_by(is_active=True).all()
r.classroom_id for r in Reservation.query.filter( available_classrooms = []
Reservation.start_time < end_dt,
Reservation.end_time > start_dt,
Reservation.status == ReservationStatus.CONFIRMED
).all()
]
available_classrooms = Classroom.query.filter( for classroom in all_classrooms:
Classroom.is_active == True, # Check if classroom has conflicting reservations
Classroom.capacity >= min_capacity, conflicts = Reservation.query.filter(
~Classroom.id.in_(conflicting_classroom_ids) Reservation.classroom_id == classroom.id,
).all() Reservation.status == ReservationStatus.CONFIRMED,
Reservation.start_time < end_dt,
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
+309 -256
View File
@@ -1,277 +1,330 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user from flask_login import login_required, current_user
from app import db
from app.models.reservation import Reservation, ReservationStatus from app.models.reservation import Reservation, ReservationStatus
from app.models.classroom import Classroom 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 app.forms.reservation import ReservationForm
from datetime import datetime, timedelta from app import db
from sqlalchemy import func, or_ from datetime import datetime, timedelta, date
from wtforms import SelectField, StringField, TextAreaField, IntegerField, SubmitField, DateTimeLocalField
from wtforms.validators import DataRequired, Length, NumberRange, Optional
schedule_bp = Blueprint('schedule', __name__, url_prefix='/schedule') schedule_bp = Blueprint('schedule', __name__)
@schedule_bp.route('/') @schedule_bp.route('/add', methods=['GET', 'POST'])
@login_required @login_required
def calendar_view(): def add_reservation():
"""Calendar view of reservations""" """Add a new reservation"""
return render_template('schedule/calendar.html') form = ReservationForm()
# Populate form choices
form.classroom_id.choices = [(c.id, f"{c.code_display} (Capacity: {c.capacity})")
for c in Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all()]
form.commission_id.choices = [(c.id, c.get_full_code())
for c in Commission.query.filter(Commission.active == True).order_by(Commission.code).all()]
if form.validate_on_submit():
try:
# Check for time conflicts
conflicting_reservations = Reservation.find_conflicts(
form.classroom_id.data,
form.start_time.data,
form.end_time.data
)
if conflicting_reservations:
flash('This classroom is already reserved for the selected time slot.', 'error')
# Get classrooms for the template
classroom_list = Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all()
return render_template('schedule/add.html', form=form, classroom_list=classroom_list, title='New Reservation')
# Check capacity
classroom = Classroom.query.get(form.classroom_id.data)
if form.expected_attendees.data > classroom.capacity:
flash(f'Expected attendees ({form.expected_attendees.data}) exceeds classroom capacity ({classroom.capacity}).', 'error')
# Get classrooms for the template
classroom_list = Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all()
return render_template('schedule/add.html', form=form, classroom_list=classroom_list, title='New Reservation')
# Create reservation
reservation = Reservation(
classroom_id=form.classroom_id.data,
commission_id=form.commission_id.data,
user_id=current_user.id,
start_time=form.start_time.data,
end_time=form.end_time.data,
purpose=form.purpose.data,
expected_attendees=form.expected_attendees.data,
notes=form.notes.data
)
db.session.add(reservation)
db.session.commit()
flash('Reservation created successfully! It will be confirmed after review.', 'success')
return redirect(url_for('schedule.list_reservations'))
except Exception as e:
db.session.rollback()
flash(f'Error creating reservation: {str(e)}', 'error')
# Get classrooms for the template
classroom_list = Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all()
return render_template('schedule/add.html', form=form, classroom_list=classroom_list, title='New Reservation')
@schedule_bp.route('/list') @schedule_bp.route('/list')
@login_required @login_required
def list_reservations(): def list_reservations():
"""List all reservations""" """List all reservations"""
page = request.args.get('page', 1, type=int)
status_filter = request.args.get('status', '')
classroom_filter = request.args.get('classroom', 0, type=int)
date_filter = request.args.get('date', '')
query = Reservation.query
# Apply filters
if status_filter:
query = query.filter(Reservation.status == status_filter)
if classroom_filter:
query = query.filter(Reservation.classroom_id == classroom_filter)
if date_filter:
try:
filter_date = datetime.fromisoformat(date_filter).date()
query = query.filter(func.date(Reservation.start_time) == filter_date)
except ValueError:
pass
reservations = query.order_by(Reservation.start_time.desc()).paginate(
page=page,
per_page=20,
error_out=False
)
# Get filter options
classrooms = Classroom.query.filter_by(is_active=True).all()
return render_template('schedule/list.html',
reservations=reservations,
classrooms=classrooms,
status_filter=status_filter,
classroom_filter=classroom_filter,
date_filter=date_filter)
@schedule_bp.route('/add', methods=['GET', 'POST'])
@login_required
def add_reservation():
"""Add new reservation"""
form = ReservationForm()
# Populate classroom choices
classrooms = Classroom.query.filter_by(is_active=True).all()
form.classroom_id.choices = [(c.id, f'{c.code} - {c.name} ({c.capacity} seats)') for c in classrooms]
# Populate commission choices
commissions = Commission.query.filter_by(is_active=True).all()
form.commission_id.choices = [(c.id, f'{c.get_full_code()} - {c.teacher_name}') for c in commissions]
if form.validate_on_submit():
# Check for conflicts
conflicts = Reservation.find_conflicts(
form.classroom_id.data,
form.start_time.data,
form.end_time.data
)
if conflicts:
flash('Classroom is already booked for this time slot.', 'error')
return render_template('schedule/add.html', form=form)
# Check if end time is after start time
if form.end_time.data <= form.start_time.data:
flash('End time must be after start time.', 'error')
return render_template('schedule/add.html', form=form)
# Check capacity
classroom = Classroom.query.get(form.classroom_id.data)
if form.expected_attendees.data > classroom.capacity:
flash('Number of attendees exceeds classroom capacity.', 'error')
return render_template('schedule/add.html', form=form)
reservation = Reservation(
classroom_id=form.classroom_id.data,
commission_id=form.commission_id.data,
user_id=current_user.id,
start_time=form.start_time.data,
end_time=form.end_time.data,
purpose=form.purpose.data,
expected_attendees=form.expected_attendees.data,
notes=form.notes.data,
status=ReservationStatus.PENDING
)
try:
db.session.add(reservation)
db.session.commit()
flash('Reservation created successfully!', 'success')
return redirect(url_for('schedule.view_reservation', Id=reservation.id))
except Exception as e:
db.session.rollback()
flash('Error creating reservation.', 'error')
return render_template('schedule/add.html', form=form)
@schedule_bp.route('/view/<int:Id>')
@login_required
def view_reservation(Id):
"""View reservation details"""
reservation = Reservation.query.get_or_404(Id)
return render_template('schedule/view.html', reservation=reservation)
@schedule_bp.route('/edit/<int:Id>', methods=['GET', 'POST'])
@login_required
def edit_reservation(Id):
"""Edit reservation"""
reservation = Reservation.query.get_or_404(Id)
# Only allow editing pending reservations
if reservation.status != ReservationStatus.PENDING:
flash('Only pending reservations can be edited.', 'warning')
return redirect(url_for('schedule.view_reservation', Id=Id))
form = ReservationForm(obj=reservation)
# Populate choices
classrooms = Classroom.query.filter_by(is_active=True).all()
form.classroom_id.choices = [(c.id, f'{c.code} - {c.name} ({c.capacity} seats)') for c in classrooms]
commissions = Commission.query.filter_by(is_active=True).all()
form.commission_id.choices = [(c.id, f'{c.get_full_code()} - {c.teacher_name}') for c in commissions]
if form.validate_on_submit():
# Check for conflicts (excluding this reservation)
conflicts = Reservation.find_conflicts(
form.classroom_id.data,
form.start_time.data,
form.end_time.data,
exclude_reservation_id=Id
)
if conflicts:
flash('Classroom is already booked for this time slot.', 'error')
return render_template('schedule/edit.html', form=form, reservation=reservation)
# Update reservation
reservation.classroom_id = form.classroom_id.data
reservation.commission_id = form.commission_id.data
reservation.start_time = form.start_time.data
reservation.end_time = form.end_time.data
reservation.purpose = form.purpose.data
reservation.expected_attendees = form.expected_attendees.data
reservation.notes = form.notes.data
reservation.updated_at = datetime.utcnow()
try:
db.session.commit()
flash('Reservation updated successfully!', 'success')
return redirect(url_for('schedule.view_reservation', Id=Id))
except Exception as e:
db.session.rollback()
flash('Error updating reservation.', 'error')
return render_template('schedule/edit.html', form=form, reservation=reservation)
@schedule_bp.route('/confirm/<int:Id>')
@login_required
def confirm_reservation(Id):
"""Confirm a reservation"""
reservation = Reservation.query.get_or_404(Id)
if reservation.status != ReservationStatus.PENDING:
flash('Only pending reservations can be confirmed.', 'warning')
return redirect(url_for('schedule.view_reservation', Id=Id))
try: try:
reservation.status = ReservationStatus.CONFIRMED page = request.args.get('page', 1, type=int)
reservation.updated_at = datetime.utcnow() status_filter = request.args.get('status', 'all')
db.session.commit() date_from = request.args.get('date_from')
flash('Reservation confirmed successfully!', 'success') date_to = request.args.get('date_to')
# Build query
query = Reservation.query
# Apply filters
if status_filter != 'all':
try:
query = query.filter_by(status=ReservationStatus[status_filter.upper()])
except KeyError:
pass
if date_from:
try:
from_date = datetime.strptime(date_from, '%Y-%m-%d')
query = query.filter(Reservation.start_time >= from_date)
except ValueError:
pass
if date_to:
try:
to_date = datetime.strptime(date_to, '%Y-%m-%d') + timedelta(days=1)
query = query.filter(Reservation.start_time < to_date)
except ValueError:
pass
# Order by start time descending
reservations = query.order_by(Reservation.start_time.desc()).paginate(
page=page, per_page=20, error_out=False
)
return render_template('schedule/list.html',
reservations=reservations,
status_filter=status_filter,
date_from=date_from,
date_to=date_to,
title='Reservations')
except Exception as e: except Exception as e:
db.session.rollback() flash(f'Error loading reservations: {str(e)}', 'error')
flash('Error confirming reservation.', 'error') return render_template('schedule/list.html', reservations=None, title='Reservations')
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)
@schedule_bp.route('/today') @schedule_bp.route('/today')
@login_required @login_required
def today_schedule(): def today_schedule():
"""Show today's schedule""" """Show today's schedule"""
today = datetime.utcnow().date() try:
today_start = datetime.combine(today, datetime.min.time()) today_start = datetime.combine(date.today(), datetime.min.time())
today_end = datetime.combine(today, datetime.max.time()) today_end = datetime.combine(date.today(), datetime.max.time())
reservations = Reservation.query.filter( # Get today's reservations
Reservation.start_time >= today_start, reservations = Reservation.query.filter(
Reservation.start_time <= today_end, Reservation.start_time >= today_start,
Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING]) Reservation.start_time <= today_end
).order_by(Reservation.start_time).all() ).order_by(Reservation.start_time).all()
return render_template('schedule/today.html', reservations=reservations) # 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 == ReservationStatus.CONFIRMED
).order_by(Reservation.start_time).all()
events = []
for reservation in reservations:
events.append({
'id': reservation.id,
'title': f"{reservation.commission.get_full_code()} - {reservation.classroom.code_display}",
'start': reservation.start_time.isoformat(),
'end': reservation.end_time.isoformat(),
'extendedProps': {
'classroom': reservation.classroom.code_display,
'purpose': reservation.purpose,
'attendees': reservation.expected_attendees,
'status': reservation.status.value
}
})
return jsonify({'events': events})
except Exception as e:
return jsonify({'error': str(e)}), 500
+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 %}
+127 -110
View File
@@ -3,124 +3,130 @@
{% block title %}Classrooms - Edu-Space Admin{% endblock %} {% block title %}Classrooms - Edu-Space Admin{% endblock %}
{% block content %} {% block content %}
<div class="row mb-4"> <div class="container-fluid pt-5 mt-4">
<div class="row">
<div class="col-12"> <div class="col-12">
<h1 class="h3 mb-0"> <div class="d-flex justify-content-between align-items-center mb-4">
<i class="bi bi-door-open text-primary"></i> Classrooms <h1 class="h3">
<small class="text-muted ms-2">Management</small> <i class="bi bi-door-open text-primary"></i> Classrooms
</h1> </h1>
</div> <div>
</div> <a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-primary">
<!-- Search and Filter -->
<div class="card border-0 shadow-sm mb-4">
<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 }}">
</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>
<div class="col-md-3">
<button type="submit" class="btn btn-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-plus-circle"></i> Add Classroom
</a> </a>
</div> </div>
</form> </div>
</div>
</div>
<!-- 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" name="search" class="form-control"
placeholder="Search by building, room, description..."
value="{{ search }}">
</div>
<div class="col-md-3">
<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-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-x-circle"></i> Clear
</a>
</div>
</form>
</div>
</div>
</div> </div>
</div> </div>
<!-- Classrooms Grid --> <!-- Classrooms Grid -->
<div class="row"> {% if classrooms and classrooms.items %}
<div class="row mt-4">
{% for classroom in classrooms.items %} {% 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 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="card-body">
<div class="row g-2"> <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"> <div class="col-6">
<div class="d-flex align-items-center"> <span class="text-muted">Capacity:</span>
<i class="bi bi-building text-primary me-2"></i> <strong>{{ classroom.capacity }}</strong>
<span class="small">{{ classroom.building }}</span>
</div>
</div> </div>
<div class="col-6"> <div class="col-6">
<div class="d-flex align-items-center"> <span class="text-muted">Building:</span>
<i class="bi bi-layers text-info me-2"></i> <strong>{{ classroom.building }}</strong>
<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>
</div> </div>
</div> </div>
{% if classroom.description %} {% if classroom.description %}
<div class="mt-3"> <p class="card-text small text-muted mt-2">
<p class="small text-muted">{{ classroom.description[:100] }}{% if classroom.description|length > 100 %}...{% endif %}</p> {{ classroom.description[:100] }}{% if classroom.description|length > 100 %}...{% endif %}
</div> </p>
{% endif %} {% endif %}
<div class="mt-3"> <!-- Resources indicator -->
<span class="badge bg-success"> <div class="mt-2">
<i class="bi bi-check-circle"></i> Active <small class="text-muted">
</span> <i class="bi bi-tools"></i>
{% if classroom.resources %}
{{ classroom.resources|length }} resources
{% else %}
No resources registered
{% endif %}
</small>
</div> </div>
</div> </div>
<div class="card-footer bg-white border-0"> <div class="card-footer bg-white border-0">
<div class="d-grid"> <div class="d-flex justify-content-between">
<a href="{{ url_for('classrooms.view_classroom', Id=classroom.id) }}" <div>
class="btn btn-outline-primary btn-sm"> <a href="{{ url_for('classrooms.view_classroom', id=classroom.id) }}"
<i class="bi bi-eye"></i> View Details class="btn btn-sm btn-outline-primary">
</a> <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> </div>
</div> </div>
@@ -130,12 +136,12 @@
<!-- Pagination --> <!-- Pagination -->
{% if classrooms.pages > 1 %} {% if classrooms.pages > 1 %}
<nav aria-label="Classrooms pagination"> <nav aria-label="Classroom pagination">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{% if classrooms.has_prev %} {% if classrooms.has_prev %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.prev_num, search=search, campus=campus) }}"> <a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.prev_num, search=search, active_only=active_only) }}">
<i class="bi bi-chevron-left"></i> Previous
</a> </a>
</li> </li>
{% endif %} {% endif %}
@@ -144,7 +150,7 @@
{% if page_num %} {% if page_num %}
{% if page_num != classrooms.page %} {% if page_num != classrooms.page %}
<li class="page-item"> <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 }} {{ page_num }}
</a> </a>
</li> </li>
@@ -155,15 +161,15 @@
{% endif %} {% endif %}
{% else %} {% else %}
<li class="page-item disabled"> <li class="page-item disabled">
<span class="page-link">...</span> <span class="page-link">…</span>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if classrooms.has_next %} {% if classrooms.has_next %}
<li class="page-item"> <li class="page-item">
<a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.next_num, search=search, campus=campus) }}"> <a class="page-link" href="{{ url_for('classrooms.list_classrooms', page=classrooms.next_num, search=search, active_only=active_only) }}">
<i class="bi bi-chevron-right"></i> Next
</a> </a>
</li> </li>
{% endif %} {% endif %}
@@ -171,14 +177,25 @@
</nav> </nav>
{% endif %} {% endif %}
{% if not classrooms.items %} {% else %}
<div class="text-center py-5"> <div class="row mt-4">
<i class="bi bi-x-circle text-muted" style="font-size: 3rem;"></i> <div class="col-12">
<h4 class="text-muted mt-3">No classrooms found</h4> <div class="text-center py-5">
<p class="text-muted">Try adjusting your search criteria or add a new classroom.</p> <i class="bi bi-door-open text-muted" style="font-size: 4rem;"></i>
<a href="{{ url_for('classrooms.add_classroom') }}" class="btn btn-primary"> <h4 class="mt-3 text-muted">No classrooms found</h4>
<i class="bi bi-plus-circle"></i> Add First Classroom <p class="text-muted">
</a> {% 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> </div>
{% endif %} {% endif %}
</div>
{% endblock %} {% 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 %}