BE-CAL-001: Add TimeSlot model with SlotType/SlotStatus/EventType enums

- New calendar.py model file with TimeSlot table definition
- SlotType enum: work, on_call, entertainment, system
- SlotStatus enum: not_started, ongoing, deferred, skipped, paused, finished, aborted
- EventType enum: job, entertainment, system_event
- All fields per design doc: user_id, date, slot_type, estimated_duration,
  scheduled_at, started_at, attended, actual_duration, event_type, event_data (JSON),
  priority, status, plan_id (FK to schedule_plans)
This commit is contained in:
zhi
2026-03-30 17:45:18 +00:00
parent 1ed7a85e11
commit 3dcd07bdf3

143
app/models/calendar.py Normal file
View File

@@ -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())