Initial commit: Parking Manager
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
This commit is contained in:
0
database/__init__.py
Normal file
0
database/__init__.py
Normal file
39
database/connection.py
Normal file
39
database/connection.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Database Connection Management
|
||||
"""
|
||||
from contextlib import contextmanager
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app import config
|
||||
|
||||
engine = create_engine(
|
||||
config.DATABASE_URL,
|
||||
connect_args={"check_same_thread": False} if "sqlite" in config.DATABASE_URL else {}
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
|
||||
|
||||
def get_db():
|
||||
"""Database session for FastAPI dependency injection"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_session():
|
||||
"""Database session for regular Python code"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Create all tables"""
|
||||
from database.models import Base
|
||||
Base.metadata.create_all(bind=engine)
|
||||
232
database/models.py
Normal file
232
database/models.py
Normal file
@@ -0,0 +1,232 @@
|
||||
"""
|
||||
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'),
|
||||
)
|
||||
Reference in New Issue
Block a user