Primo commit

This commit is contained in:
2026-01-13 11:20:12 +01:00
parent ce9e2fdf2a
commit 17453f5d13
51 changed files with 3883 additions and 2508 deletions

View File

@@ -2,12 +2,73 @@
SQLAlchemy ORM Models
Clean, focused data models for parking management
"""
from sqlalchemy import Column, String, Integer, Text, ForeignKey, Index
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"
@@ -16,34 +77,30 @@ class User(Base):
email = Column(Text, unique=True, nullable=False)
password_hash = Column(Text)
name = Column(Text)
role = Column(Text, nullable=False, default="employee") # admin, manager, employee
manager_id = Column(Text, ForeignKey("users.id")) # Who manages this user (any user can have a manager)
# 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.
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, ..., 6=Saturday
week_start_day = Column(Integer, default=0) # 0=Sunday, 1=Monday, ... (Matches WeekDay logic)
# 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_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(Integer, default=1) # Immediate notification on assignment changes
notify_parking_changes = Column(Boolean, default=True) # Immediate notification on assignment changes
created_at = Column(Text)
updated_at = Column(Text)
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationships
manager = relationship("User", remote_side=[id], backref="managed_users")
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_manager', 'manager_id'),
Index('idx_user_office', 'office_id'),
)
@@ -53,10 +110,10 @@ class UserPresence(Base):
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)
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")
@@ -68,97 +125,100 @@ class UserPresence(Base):
class DailyParkingAssignment(Base):
"""Parking spot assignments per day - spots belong to managers"""
"""Parking spot assignments per day - spots belong to offices"""
__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)
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"))
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False) # Manager who owns the spot
created_at = Column(Text)
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])
manager = relationship("User", foreign_keys=[manager_id])
office = relationship("Office")
__table_args__ = (
Index('idx_assignment_manager_date', 'manager_id', 'date'),
Index('idx_assignment_office_date', 'office_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 parking pool (holidays, special closures)"""
__tablename__ = "manager_closing_days"
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)
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
date = Column(Text, nullable=False) # YYYY-MM-DD
office_id = Column(Text, ForeignKey("offices.id", ondelete="CASCADE"), nullable=False)
date = Column(Date, nullable=False)
end_date = Column(Date)
reason = Column(Text)
# Relationships
manager = relationship("User")
office = relationship("Office", back_populates="closing_days")
__table_args__ = (
Index('idx_closing_manager_date', 'manager_id', 'date', unique=True),
Index('idx_closing_office_date', 'office_id', 'date', unique=True),
)
class ManagerWeeklyClosingDay(Base):
"""Weekly recurring closing days for a manager's parking pool (e.g., Saturday and Sunday)"""
__tablename__ = "manager_weekly_closing_days"
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)
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
weekday = Column(Integer, nullable=False) # 0=Sunday, 1=Monday, ..., 6=Saturday
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
manager = relationship("User")
office = relationship("Office", back_populates="weekly_closing_days")
__table_args__ = (
Index('idx_weekly_closing_manager_day', 'manager_id', 'weekday', unique=True),
Index('idx_weekly_closing_office_day', 'office_id', 'weekday', unique=True),
)
class ParkingGuarantee(Base):
"""Users guaranteed a parking spot when present (set by manager)"""
"""Users guaranteed a parking spot when present (set by office manager)"""
__tablename__ = "parking_guarantees"
id = Column(Text, primary_key=True)
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
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(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)
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
manager = relationship("User", foreign_keys=[manager_id])
office = relationship("Office")
user = relationship("User", foreign_keys=[user_id])
__table_args__ = (
Index('idx_guarantee_manager_user', 'manager_id', 'user_id', unique=True),
Index('idx_guarantee_office_user', 'office_id', 'user_id', unique=True),
)
class ParkingExclusion(Base):
"""Users excluded from parking assignment (set by manager)"""
"""Users excluded from parking assignment (set by office manager)"""
__tablename__ = "parking_exclusions"
id = Column(Text, primary_key=True)
manager_id = Column(Text, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
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(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)
start_date = Column(Date) # Optional
end_date = Column(Date) # Optional
notes = Column(Text, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)
# Relationships
manager = relationship("User", foreign_keys=[manager_id])
office = relationship("Office")
user = relationship("User", foreign_keys=[user_id])
__table_args__ = (
Index('idx_exclusion_manager_user', 'manager_id', 'user_id', unique=True),
Index('idx_exclusion_office_user', 'office_id', 'user_id', unique=True),
)
@@ -168,9 +228,9 @@ class NotificationLog(Base):
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)
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'),
@@ -183,11 +243,11 @@ class NotificationQueue(Base):
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
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(Text, nullable=False)
sent_at = Column(Text) # null = not sent yet
created_at = Column(DateTime, default=datetime.utcnow)
sent_at = Column(DateTime) # null = not sent yet
__table_args__ = (
Index('idx_queue_pending', 'sent_at'),