feat: implementacion de dashboards por rol, soporte de clases virtuales concurrentes en importador de sheets y cartelera del dia
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
from app import db
|
||||
from datetime import datetime
|
||||
|
||||
class Classroom(db.Model):
|
||||
__tablename__ = 'classrooms'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
campus_id = db.Column(db.Integer, nullable=True) # Maps to campus_id in DB
|
||||
building_id = db.Column(db.Integer, db.ForeignKey('buildings.id'), nullable=True)
|
||||
building = db.Column(db.String(100), nullable=False)
|
||||
code = db.Column('room_number', db.String(50), nullable=False) # Maps to room_number column in DB
|
||||
floor = db.Column(db.String(50), nullable=False, default='1')
|
||||
capacity = db.Column(db.Integer, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
is_active = db.Column('active', db.Boolean, default=True, nullable=False) # Maps to active column in DB
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
|
||||
# Relación con Building (mapeada vía backref 'building_entity' en Building.classrooms)
|
||||
@property
|
||||
def building_rel(self):
|
||||
return self.building_entity
|
||||
|
||||
@property
|
||||
def building_name(self):
|
||||
"""Nombre del edificio desacoplado"""
|
||||
if self.building_rel:
|
||||
return self.building_rel.name
|
||||
return self.building or 'Edificio Central'
|
||||
|
||||
@property
|
||||
def floor_display(self):
|
||||
"""Formato amigable para el piso: PB, Planta Baja, Piso X"""
|
||||
f = str(self.floor).strip() if self.floor is not None else ''
|
||||
if f in ['0', 'PB', 'pb', 'Planta Baja', 'planta baja']:
|
||||
return 'Planta Baja'
|
||||
if f.lower().startswith('piso') or '°' in f:
|
||||
return f
|
||||
return f"Piso {f}"
|
||||
|
||||
@property
|
||||
def is_virtual(self):
|
||||
"""Indica si el aula es virtual"""
|
||||
return (self.code and 'VIRTUAL' in self.code.upper()) or (self.building and 'VIRTUAL' in self.building.upper())
|
||||
|
||||
@property
|
||||
def location_display(self):
|
||||
"""Ubicación completa y limpia: Edificio · Piso · Aula"""
|
||||
if self.is_virtual:
|
||||
return '🌐 Campus Virtual'
|
||||
return f"{self.building_name} · {self.floor_display} · {self.code}"
|
||||
|
||||
# Virtual properties for compatibility
|
||||
@property
|
||||
def name(self):
|
||||
"""Generate name from building and code"""
|
||||
return f"{self.building_name}-{self.code}"
|
||||
|
||||
@property
|
||||
def campus_location(self):
|
||||
"""Placeholder for campus location"""
|
||||
return self.building_rel.address if self.building_rel else None
|
||||
|
||||
# Virtual field for compatibility - combines building and code
|
||||
@property
|
||||
def room_number(self):
|
||||
return self.code
|
||||
|
||||
@room_number.setter
|
||||
def room_number(self, value):
|
||||
self.code = value
|
||||
|
||||
# Override code property for consistency
|
||||
@property
|
||||
def code_display(self):
|
||||
return f"{self.building_name} - {self.code}"
|
||||
|
||||
@property
|
||||
def capacity_display(self):
|
||||
"""Retorna '∞' para aulas virtuales o capacidad ilimitada, o el número entero."""
|
||||
if self.capacity == 0 or self.is_virtual:
|
||||
return '∞'
|
||||
return str(self.capacity)
|
||||
|
||||
@property
|
||||
def is_unlimited(self):
|
||||
return self.capacity == 0 or self.is_virtual
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert classroom to dictionary"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'building_id': self.building_id,
|
||||
'building': self.building_name,
|
||||
'code': self.code,
|
||||
'code_display': self.code_display,
|
||||
'name': self.name,
|
||||
'floor': self.floor,
|
||||
'floor_display': self.floor_display,
|
||||
'location_display': self.location_display,
|
||||
'capacity': self.capacity,
|
||||
'capacity_display': self.capacity_display,
|
||||
'is_virtual': self.is_virtual,
|
||||
'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] if hasattr(self, 'resources') else []
|
||||
}
|
||||
|
||||
def get_full_location(self):
|
||||
"""Get full classroom location"""
|
||||
if self.is_virtual:
|
||||
return 'Campus Virtual (Remoto)'
|
||||
return f"{self.building_name} - {self.floor_display}"
|
||||
|
||||
@staticmethod
|
||||
def find_best_classroom(max_students, start_time, end_time, exclude_reservation_id=None):
|
||||
"""Find the best suitable classroom for a commission based on capacity and availability"""
|
||||
from app.models.reservation import Reservation, ReservationStatus
|
||||
|
||||
# Get all active classrooms with sufficient capacity
|
||||
suitable_classrooms = Classroom.query.filter(
|
||||
Classroom.is_active == True,
|
||||
Classroom.capacity >= max_students
|
||||
).order_by(Classroom.capacity.asc()).all() # Start with smallest suitable
|
||||
|
||||
# Check availability for each classroom
|
||||
available_classrooms = []
|
||||
for classroom in suitable_classrooms:
|
||||
# Check for conflicts
|
||||
conflicts = Reservation.find_conflicts(
|
||||
classroom.id, start_time, end_time, exclude_reservation_id
|
||||
)
|
||||
|
||||
if not conflicts:
|
||||
# Calculate efficiency score (how well the capacity matches)
|
||||
efficiency = classroom.capacity / max_students if max_students > 0 else float('inf')
|
||||
available_classrooms.append({
|
||||
'classroom': classroom,
|
||||
'efficiency': efficiency,
|
||||
'wasted_capacity': classroom.capacity - max_students
|
||||
})
|
||||
|
||||
# Sort by efficiency (closest capacity match first)
|
||||
available_classrooms.sort(key=lambda x: x['efficiency'])
|
||||
|
||||
return available_classrooms
|
||||
|
||||
@staticmethod
|
||||
def get_availability_status(start_time, end_time, exclude_reservation_id=None):
|
||||
"""Get all classrooms classified as available or occupied for a time slot"""
|
||||
from app.models.reservation import Reservation, ReservationStatus
|
||||
|
||||
all_classrooms = Classroom.query.filter_by(is_active=True).all()
|
||||
available = []
|
||||
occupied = []
|
||||
|
||||
for classroom in all_classrooms:
|
||||
conflicts = Reservation.find_conflicts(
|
||||
classroom.id, start_time, end_time, exclude_reservation_id
|
||||
)
|
||||
|
||||
classroom_info = {
|
||||
'classroom': classroom,
|
||||
'conflicts': conflicts
|
||||
}
|
||||
|
||||
if conflicts:
|
||||
occupied.append(classroom_info)
|
||||
else:
|
||||
available.append(classroom_info)
|
||||
|
||||
return {
|
||||
'available': available,
|
||||
'occupied': occupied,
|
||||
'total_available': len(available),
|
||||
'total_occupied': len(occupied)
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Classroom {self.code_display}>'
|
||||
|
||||
class ClassroomResource(db.Model):
|
||||
__tablename__ = 'classroom_resources'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
classroom_id = db.Column(db.Integer, db.ForeignKey('classrooms.id'), nullable=False)
|
||||
resource_type = db.Column(db.String(50), nullable=False) # PROJECTOR, WHITEBOARD, COMPUTER, etc.
|
||||
brand = db.Column(db.String(100))
|
||||
model = db.Column(db.String(100))
|
||||
specifications = db.Column(db.Text)
|
||||
functional = db.Column(db.Boolean, default=True)
|
||||
last_maintenance_date = db.Column(db.DateTime)
|
||||
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 {
|
||||
'id': self.id,
|
||||
'classroom_id': self.classroom_id,
|
||||
'resource_type': self.resource_type,
|
||||
'brand': self.brand,
|
||||
'model': self.model,
|
||||
'specifications': self.specifications,
|
||||
'functional': self.functional,
|
||||
'last_maintenance_date': self.last_maintenance_date.isoformat() if self.last_maintenance_date else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Resource {self.resource_type} for Classroom {self.classroom_id}>'
|
||||
Reference in New Issue
Block a user