diff --git a/app/models/calendar.py b/app/models/calendar.py new file mode 100644 index 0000000..e74c7f6 --- /dev/null +++ b/app/models/calendar.py @@ -0,0 +1,143 @@ +"""Calendar models — TimeSlot and related enums. + +TimeSlot represents a single scheduled slot on a user's calendar. +Slots can be created manually or materialized from a SchedulePlan. + +See: NEXT_WAVE_DEV_DIRECTION.md §1.1 +""" + +from sqlalchemy import ( + Column, Integer, String, Text, DateTime, Date, Time, + ForeignKey, Enum, Boolean, JSON, +) +from sqlalchemy.sql import func +from app.core.config import Base +import enum + + +# --------------------------------------------------------------------------- +# Enums +# --------------------------------------------------------------------------- + +class SlotType(str, enum.Enum): + """What kind of slot this is.""" + WORK = "work" + ON_CALL = "on_call" + ENTERTAINMENT = "entertainment" + SYSTEM = "system" + + +class SlotStatus(str, enum.Enum): + """Lifecycle status of a slot.""" + NOT_STARTED = "not_started" + ONGOING = "ongoing" + DEFERRED = "deferred" + SKIPPED = "skipped" + PAUSED = "paused" + FINISHED = "finished" + ABORTED = "aborted" + + +class EventType(str, enum.Enum): + """High-level event category stored alongside the slot.""" + JOB = "job" + ENTERTAINMENT = "entertainment" + SYSTEM_EVENT = "system_event" + + +# --------------------------------------------------------------------------- +# TimeSlot model +# --------------------------------------------------------------------------- + +class TimeSlot(Base): + __tablename__ = "time_slots" + + id = Column(Integer, primary_key=True, index=True) + + user_id = Column( + Integer, + ForeignKey("users.id"), + nullable=False, + index=True, + comment="Owner of this slot", + ) + + date = Column( + Date, + nullable=False, + index=True, + comment="Calendar date for this slot", + ) + + slot_type = Column( + Enum(SlotType, values_callable=lambda x: [e.value for e in x]), + nullable=False, + comment="work | on_call | entertainment | system", + ) + + estimated_duration = Column( + Integer, + nullable=False, + comment="Estimated duration in minutes (1-50)", + ) + + scheduled_at = Column( + Time, + nullable=False, + comment="Planned start time (00:00-23:00)", + ) + + started_at = Column( + Time, + nullable=True, + comment="Actual start time (filled when slot begins)", + ) + + attended = Column( + Boolean, + default=False, + nullable=False, + comment="Whether the slot has been attended", + ) + + actual_duration = Column( + Integer, + nullable=True, + comment="Actual duration in minutes (0-65535), no upper design limit", + ) + + event_type = Column( + Enum(EventType, values_callable=lambda x: [e.value for e in x]), + nullable=True, + comment="job | entertainment | system_event", + ) + + event_data = Column( + JSON, + nullable=True, + comment="Event details JSON — structure depends on event_type", + ) + + priority = Column( + Integer, + nullable=False, + default=0, + comment="Priority 0-99, higher = more important", + ) + + status = Column( + Enum(SlotStatus, values_callable=lambda x: [e.value for e in x]), + nullable=False, + default=SlotStatus.NOT_STARTED, + comment="Lifecycle status of this slot", + ) + + plan_id = Column( + Integer, + ForeignKey("schedule_plans.id"), + nullable=True, + comment="Source plan if materialized from a SchedulePlan; set NULL on edit/cancel", + ) + + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), onupdate=func.now())