from app import db from datetime import datetime, timedelta from enum import Enum from sqlalchemy.types import TypeDecorator, String class StatusString(str): """String wrapper providing .value property for backward compatibility with Enum usage""" @property def value(self): return str(self) def __eq__(self, other): if hasattr(other, 'value'): return str(self) == str(other.value) return str(self) == str(other) class StatusType(TypeDecorator): impl = String(20) cache_ok = True def process_result_value(self, value, dialect): return StatusString(value) if value is not None else None def process_bind_param(self, value, dialect): if hasattr(value, 'value'): return str(value.value) return str(value) if value is not None else None class ReservationStatus(Enum): PENDING = 'PENDING' CONFIRMED = 'CONFIRMED' CANCELLED = 'CANCELLED' COMPLETED = 'COMPLETED' NO_SHOW = 'NO_SHOW' class Reservation(db.Model): __tablename__ = 'reservations' id = db.Column(db.Integer, primary_key=True) classroom_id = db.Column(db.Integer, db.ForeignKey('classrooms.id'), nullable=False) commission_id = db.Column(db.Integer, db.ForeignKey('commissions.id'), nullable=False) user_id = db.Column('teacher_id', db.Integer, db.ForeignKey('users.id'), nullable=False) # Maps to teacher_id in DB start_time = db.Column(db.DateTime, nullable=False) end_time = db.Column(db.DateTime, nullable=False) purpose = db.Column(db.String(500), nullable=False) status = db.Column(StatusType, default='PENDING', nullable=False) # Maps to VARCHAR(20) with StatusString expected_attendees = db.Column(db.Integer, nullable=False) shift = db.Column(db.String(30), nullable=True) # MaƱana, Tarde, Vespertino, Noche notes = db.Column(db.Text) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Virtual property to convert status string to enum @property def status_enum(self): """Get status as enum""" try: return ReservationStatus(str(self.status)) except (ValueError, AttributeError): return ReservationStatus.PENDING @property def subject_code(self): """Get exact subject code (e.g. INGA0003, ASIG00165)""" if self.commission and self.commission.subject: return self.commission.subject.code return '' @property def subject_name(self): """Get subject name""" if self.commission and self.commission.subject: return self.commission.subject.name return self.purpose or '' @property def classroom_name(self): """Get classroom code or name""" if self.classroom: return self.classroom.code return '' # Relationships classroom = db.relationship('Classroom', backref=db.backref('reservation_list', lazy=True, cascade='all, delete-orphan')) commission = db.relationship('Commission', backref=db.backref('reservation_list', lazy=True, cascade='all, delete-orphan')) # User already defines the relationship back to reservation def to_dict(self): """Convert reservation to dictionary""" return { 'id': self.id, 'classroom_id': self.classroom_id, 'commission_id': self.commission_id, 'user_id': self.user_id, 'start_time': self.start_time.isoformat() if self.start_time else None, 'end_time': self.end_time.isoformat() if self.end_time else None, 'purpose': self.purpose, 'status': self.status, # Already a string in DB 'expected_attendees': self.expected_attendees, 'notes': self.notes, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None, 'classroom': self.classroom.to_dict() if self.classroom else None, 'commission': self.commission.to_dict() if self.commission else None, 'user': self.user.to_dict() if self.user else None } def is_confirmed(self): """Check if reservation is confirmed""" return self.status == ReservationStatus.CONFIRMED.value def can_be_cancelled(self): """Check if reservation can be cancelled (24 hours before start)""" from datetime import timedelta return self.start_time > datetime.utcnow() + timedelta(hours=24) def check_conflict(self, other_reservation): """Check if this reservation conflicts with another""" if self.classroom_id != other_reservation.classroom_id: return False if self.status != ReservationStatus.CONFIRMED.value: return False if other_reservation.status != ReservationStatus.CONFIRMED.value: return False # Check time overlap return (self.start_time < other_reservation.end_time and self.end_time > other_reservation.start_time) @staticmethod def find_conflicts(classroom_id, start_time, end_time, exclude_reservation_id=None): """Find conflicting reservations for a specific classroom and time slot""" query = Reservation.query.filter( Reservation.classroom_id == classroom_id, Reservation.status == ReservationStatus.CONFIRMED.value, Reservation.start_time < end_time, Reservation.end_time > start_time ) if exclude_reservation_id: query = query.filter(Reservation.id != exclude_reservation_id) return query.all() def __repr__(self): return f''