Features: - Manager-centric parking spot management - Fair assignment algorithm (parking/presence ratio) - Presence tracking calendar - Closing days (specific & weekly recurring) - Guarantees and exclusions - Authelia/LLDAP integration for SSO Stack: - FastAPI backend - SQLite database - Vanilla JS frontend - Docker deployment
233 lines
8.7 KiB
Python
233 lines
8.7 KiB
Python
"""
|
|
SQLAlchemy ORM Models
|
|
Clean, focused data models for parking management
|
|
"""
|
|
from sqlalchemy import Column, String, Integer, Text, ForeignKey, Index
|
|
from sqlalchemy.orm import relationship, declarative_base
|
|
|
|
Base = declarative_base()
|
|
|
|
|
|
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(Text, nullable=False, default="employee") # admin, manager, employee
|
|
office_id = Column(Text, ForeignKey("offices.id"))
|
|
|
|
# Manager-specific fields (only relevant for role='manager')
|
|
manager_parking_quota = Column(Integer, default=0) # Number of spots this manager controls
|
|
manager_spot_prefix = Column(Text) # Letter prefix for spots: A, B, C, etc.
|
|
|
|
# User preferences
|
|
week_start_day = Column(Integer, default=0) # 0=Sunday, 1=Monday, ..., 6=Saturday
|
|
|
|
# Notification preferences
|
|
notify_weekly_parking = Column(Integer, default=1) # Weekly parking summary (Friday at 12)
|
|
notify_daily_parking = Column(Integer, default=1) # 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(Integer, default=1) # Immediate notification on assignment changes
|
|
|
|
created_at = Column(Text)
|
|
updated_at = Column(Text)
|
|
|
|
# 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")
|
|
managed_offices = relationship("OfficeMembership", back_populates="user", cascade="all, delete-orphan")
|
|
|
|
__table_args__ = (
|
|
Index('idx_user_email', 'email'),
|
|
Index('idx_user_office', 'office_id'),
|
|
)
|
|
|
|
|
|
class Office(Base):
|
|
"""Office locations - containers for grouping employees"""
|
|
__tablename__ = "offices"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
name = Column(Text, nullable=False)
|
|
location = Column(Text)
|
|
# Note: parking_spots removed - spots are now managed at manager level
|
|
|
|
created_at = Column(Text)
|
|
updated_at = Column(Text)
|
|
|
|
# Relationships
|
|
users = relationship("User", back_populates="office")
|
|
managers = relationship("OfficeMembership", back_populates="office", cascade="all, delete-orphan")
|
|
|
|
|
|
class OfficeMembership(Base):
|
|
"""Manager-Office relationship (which managers manage which offices)"""
|
|
__tablename__ = "office_memberships"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
|
|
created_at = Column(Text)
|
|
|
|
# Relationships
|
|
user = relationship("User", back_populates="managed_offices")
|
|
office = relationship("Office", back_populates="managers")
|
|
|
|
__table_args__ = (
|
|
Index('idx_membership_user', 'user_id'),
|
|
Index('idx_membership_office', 'office_id'),
|
|
Index('idx_membership_unique', 'user_id', 'office_id', unique=True),
|
|
)
|
|
|
|
|
|
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(Text, nullable=False) # YYYY-MM-DD
|
|
status = Column(Text, nullable=False) # present, remote, absent
|
|
created_at = Column(Text)
|
|
updated_at = Column(Text)
|
|
|
|
# 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 managers"""
|
|
__tablename__ = "daily_parking_assignments"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
date = Column(Text, nullable=False) # YYYY-MM-DD
|
|
spot_id = Column(Text, nullable=False) # A1, A2, B1, B2, etc. (prefix from manager)
|
|
user_id = Column(Text, ForeignKey("users.id", ondelete="SET NULL"))
|
|
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) # Manager who owns the spot
|
|
created_at = Column(Text)
|
|
|
|
# Relationships
|
|
user = relationship("User", back_populates="assignments", foreign_keys=[user_id])
|
|
manager = relationship("User", foreign_keys=[manager_id])
|
|
|
|
__table_args__ = (
|
|
Index('idx_assignment_manager_date', 'manager_id', 'date'),
|
|
Index('idx_assignment_user', 'user_id'),
|
|
Index('idx_assignment_date_spot', 'date', 'spot_id'),
|
|
)
|
|
|
|
|
|
class ManagerClosingDay(Base):
|
|
"""Specific date closing days for a manager's offices (holidays, special closures)"""
|
|
__tablename__ = "manager_closing_days"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
date = Column(Text, nullable=False) # YYYY-MM-DD
|
|
reason = Column(Text)
|
|
|
|
# Relationships
|
|
manager = relationship("User")
|
|
|
|
__table_args__ = (
|
|
Index('idx_closing_manager_date', 'manager_id', 'date', unique=True),
|
|
)
|
|
|
|
|
|
class ManagerWeeklyClosingDay(Base):
|
|
"""Weekly recurring closing days for a manager's offices (e.g., Saturday and Sunday)"""
|
|
__tablename__ = "manager_weekly_closing_days"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
weekday = Column(Integer, nullable=False) # 0=Sunday, 1=Monday, ..., 6=Saturday
|
|
|
|
# Relationships
|
|
manager = relationship("User")
|
|
|
|
__table_args__ = (
|
|
Index('idx_weekly_closing_manager_day', 'manager_id', 'weekday', unique=True),
|
|
)
|
|
|
|
|
|
class ParkingGuarantee(Base):
|
|
"""Users guaranteed a parking spot when present (set by manager)"""
|
|
__tablename__ = "parking_guarantees"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
start_date = Column(Text) # Optional: YYYY-MM-DD (null = no start limit)
|
|
end_date = Column(Text) # Optional: YYYY-MM-DD (null = no end limit)
|
|
created_at = Column(Text)
|
|
|
|
# Relationships
|
|
manager = relationship("User", foreign_keys=[manager_id])
|
|
user = relationship("User", foreign_keys=[user_id])
|
|
|
|
__table_args__ = (
|
|
Index('idx_guarantee_manager_user', 'manager_id', 'user_id', unique=True),
|
|
)
|
|
|
|
|
|
class ParkingExclusion(Base):
|
|
"""Users excluded from parking assignment (set by manager)"""
|
|
__tablename__ = "parking_exclusions"
|
|
|
|
id = Column(Text, primary_key=True)
|
|
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
user_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
|
|
start_date = Column(Text) # Optional: YYYY-MM-DD (null = no start limit)
|
|
end_date = Column(Text) # Optional: YYYY-MM-DD (null = no end limit)
|
|
created_at = Column(Text)
|
|
|
|
# Relationships
|
|
manager = relationship("User", foreign_keys=[manager_id])
|
|
user = relationship("User", foreign_keys=[user_id])
|
|
|
|
__table_args__ = (
|
|
Index('idx_exclusion_manager_user', 'manager_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(Text, nullable=False) # presence_reminder, weekly_parking, daily_parking, parking_change
|
|
reference_date = Column(Text) # The date/week this notification refers to (YYYY-MM-DD or YYYY-Www)
|
|
sent_at = Column(Text, nullable=False)
|
|
|
|
__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(Text, nullable=False) # parking_change
|
|
subject = Column(Text, nullable=False)
|
|
body = Column(Text, nullable=False)
|
|
created_at = Column(Text, nullable=False)
|
|
sent_at = Column(Text) # null = not sent yet
|
|
|
|
__table_args__ = (
|
|
Index('idx_queue_pending', 'sent_at'),
|
|
)
|