255 lines
9.6 KiB
Python
255 lines
9.6 KiB
Python
"""
|
|
SQLAlchemy ORM Models
|
|
Clean, focused data models for parking management
|
|
"""
|
|
import enum
|
|
from sqlalchemy import Column, String, Integer, Text, ForeignKey, Index, Enum, Date, DateTime, Boolean
|
|
from sqlalchemy.orm import relationship, declarative_base
|
|
from datetime import datetime, date
|
|
|
|
Base = declarative_base()
|
|
|
|
|
|
class UserRole(str, enum.Enum):
|
|
ADMIN = "admin"
|
|
MANAGER = "manager"
|
|
EMPLOYEE = "employee"
|
|
|
|
|
|
class PresenceStatus(str, enum.Enum):
|
|
PRESENT = "present"
|
|
REMOTE = "remote"
|
|
ABSENT = "absent"
|
|
|
|
|
|
class NotificationType(str, enum.Enum):
|
|
PRESENCE_REMINDER = "presence_reminder"
|
|
WEEKLY_PARKING = "weekly_parking"
|
|
DAILY_PARKING = "daily_parking"
|
|
PARKING_CHANGE = "parking_change"
|
|
|
|
|
|
class WeekDay(enum.IntEnum):
|
|
# Matches Python's calendar (0=Monday)? No!
|
|
# The current DB convention in ManagerWeeklyClosingDay seems to be 0=Sunday based on comment:
|
|
# "0=Sunday, 1=Monday, ..., 6=Saturday"
|
|
# To keep consistency with existing logic comments, we'll stick to that,
|
|
# OR we can switch to standard Python (0=Monday).
|
|
# Plan said: "IntEnum matching DB convention (0=Sunday, 1=Monday, ...)"
|
|
MONDAY = 0
|
|
TUESDAY = 1
|
|
WEDNESDAY = 2
|
|
THURSDAY = 3
|
|
FRIDAY = 4
|
|
SATURDAY = 5
|
|
SUNDAY = 6
|
|
|
|
|
|
|
|
class Office(Base):
|
|
"""Organization units that have parking spots"""
|
|
__tablename__ = "offices"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
name = Column(Text, nullable=False)
|
|
parking_quota = Column(Integer, default=0)
|
|
spot_prefix = Column(Text) # Letter prefix: A, B, C
|
|
|
|
# Booking Window Settings (Batch Assignment)
|
|
booking_window_enabled = Column(Boolean, default=False)
|
|
booking_window_end_hour = Column(Integer, default=18) # 0-23
|
|
booking_window_end_minute = Column(Integer, default=0) # 0-59
|
|
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
# Relationships
|
|
users = relationship("User", back_populates="office")
|
|
closing_days = relationship("OfficeClosingDay", back_populates="office", cascade="all, delete-orphan")
|
|
weekly_closing_days = relationship("OfficeWeeklyClosingDay", back_populates="office", cascade="all, delete-orphan")
|
|
|
|
|
|
class User(Base):
|
|
"""Application users"""
|
|
__tablename__ = "users"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
email = Column(Text, unique=True, nullable=False)
|
|
password_hash = Column(Text)
|
|
name = Column(Text)
|
|
role = Column(Enum(UserRole, values_callable=lambda obj: [e.value for e in obj]), nullable=False, default=UserRole.EMPLOYEE)
|
|
office_id = Column(Text, ForeignKey("offices.id")) # Which office this user belongs to
|
|
|
|
# User preferences
|
|
week_start_day = Column(Integer, default=0) # 0=Sunday, 1=Monday, ... (Matches WeekDay logic)
|
|
|
|
# Notification preferences
|
|
notify_weekly_parking = Column(Boolean, default=True) # Weekly parking summary (Friday at 12)
|
|
notify_daily_parking = Column(Boolean, default=True) # Daily parking reminder
|
|
notify_daily_parking_hour = Column(Integer, default=8) # Hour for daily reminder (0-23)
|
|
notify_daily_parking_minute = Column(Integer, default=0) # Minute for daily reminder (0-59)
|
|
notify_parking_changes = Column(Boolean, default=True) # Immediate notification on assignment changes
|
|
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
# Relationships
|
|
office = relationship("Office", back_populates="users")
|
|
presences = relationship("UserPresence", back_populates="user", cascade="all, delete-orphan")
|
|
assignments = relationship("DailyParkingAssignment", back_populates="user", foreign_keys="DailyParkingAssignment.user_id")
|
|
|
|
__table_args__ = (
|
|
Index('idx_user_email', 'email'),
|
|
Index('idx_user_office', 'office_id'),
|
|
)
|
|
|
|
|
|
class UserPresence(Base):
|
|
"""Daily presence records"""
|
|
__tablename__ = "user_presences"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
date = Column(Date, nullable=False)
|
|
status = Column(Enum(PresenceStatus, values_callable=lambda obj: [e.value for e in obj]), nullable=False) # present, remote, absent
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
# Relationships
|
|
user = relationship("User", back_populates="presences")
|
|
|
|
__table_args__ = (
|
|
Index('idx_presence_user_date', 'user_id', 'date', unique=True),
|
|
Index('idx_presence_date', 'date'),
|
|
)
|
|
|
|
|
|
class DailyParkingAssignment(Base):
|
|
"""Parking spot assignments per day - spots belong to offices"""
|
|
__tablename__ = "daily_parking_assignments"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
date = Column(Date, nullable=False)
|
|
spot_id = Column(Text, nullable=False) # A1, A2, B1, B2, etc. (prefix from office)
|
|
user_id = Column(Text, ForeignKey("users.id", ondelete="SET NULL"))
|
|
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False) # Office that owns the spot
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
# Relationships
|
|
user = relationship("User", back_populates="assignments", foreign_keys=[user_id])
|
|
office = relationship("Office")
|
|
|
|
__table_args__ = (
|
|
Index('idx_assignment_office_date', 'office_id', 'date'),
|
|
Index('idx_assignment_user', 'user_id'),
|
|
Index('idx_assignment_date_spot', 'date', 'spot_id'),
|
|
)
|
|
|
|
|
|
class OfficeClosingDay(Base):
|
|
"""Specific date closing days for an office's parking pool (holidays, special closures)"""
|
|
__tablename__ = "office_closing_days"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
|
|
date = Column(Date, nullable=False)
|
|
end_date = Column(Date)
|
|
reason = Column(Text)
|
|
|
|
# Relationships
|
|
office = relationship("Office", back_populates="closing_days")
|
|
|
|
__table_args__ = (
|
|
Index('idx_closing_office_date', 'office_id', 'date', unique=True),
|
|
)
|
|
|
|
|
|
class OfficeWeeklyClosingDay(Base):
|
|
"""Weekly recurring closing days for an office's parking pool (e.g., Saturday and Sunday)"""
|
|
__tablename__ = "office_weekly_closing_days"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
|
|
weekday = Column(Integer, nullable=False) # 0=Sunday, 1=Monday, ..., 6=Saturday (Matches WeekDay Enum logic)
|
|
|
|
# Relationships
|
|
office = relationship("Office", back_populates="weekly_closing_days")
|
|
|
|
__table_args__ = (
|
|
Index('idx_weekly_closing_office_day', 'office_id', 'weekday', unique=True),
|
|
)
|
|
|
|
|
|
class ParkingGuarantee(Base):
|
|
"""Users guaranteed a parking spot when present (set by office manager)"""
|
|
__tablename__ = "parking_guarantees"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
|
|
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
start_date = Column(Date) # Optional (null = no start limit)
|
|
end_date = Column(Date) # Optional (null = no end limit)
|
|
notes = Column(Text, nullable=True)
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
# Relationships
|
|
office = relationship("Office")
|
|
user = relationship("User", foreign_keys=[user_id])
|
|
|
|
__table_args__ = (
|
|
Index('idx_guarantee_office_user', 'office_id', 'user_id', unique=True),
|
|
)
|
|
|
|
|
|
class ParkingExclusion(Base):
|
|
"""Users excluded from parking assignment (set by office manager)"""
|
|
__tablename__ = "parking_exclusions"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
|
|
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
start_date = Column(Date) # Optional
|
|
end_date = Column(Date) # Optional
|
|
notes = Column(Text, nullable=True)
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
# Relationships
|
|
office = relationship("Office")
|
|
user = relationship("User", foreign_keys=[user_id])
|
|
|
|
__table_args__ = (
|
|
Index('idx_exclusion_office_user', 'office_id', 'user_id', unique=True),
|
|
)
|
|
|
|
|
|
class NotificationLog(Base):
|
|
"""Log of sent notifications to prevent duplicates"""
|
|
__tablename__ = "notification_logs"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
notification_type = Column(Enum(NotificationType, values_callable=lambda obj: [e.value for e in obj]), nullable=False)
|
|
reference_date = Column(Text) # The date/week this notification refers to (YYYY-MM-DD or YYYY-Www) - keeping as Text for flexibility
|
|
sent_at = Column(DateTime, default=datetime.utcnow)
|
|
|
|
__table_args__ = (
|
|
Index('idx_notification_user_type_date', 'user_id', 'notification_type', 'reference_date'),
|
|
)
|
|
|
|
|
|
class NotificationQueue(Base):
|
|
"""Queue for pending notifications (for immediate parking change notifications)"""
|
|
__tablename__ = "notification_queue"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
notification_type = Column(Enum(NotificationType, values_callable=lambda obj: [e.value for e in obj]), nullable=False) # parking_change
|
|
subject = Column(Text, nullable=False)
|
|
body = Column(Text, nullable=False)
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
sent_at = Column(DateTime) # null = not sent yet
|
|
|
|
__table_args__ = (
|
|
Index('idx_queue_pending', 'sent_at'),
|
|
)
|