//! Event domain types and DTOs. //! //! Events represent calendar entries that can be standalone or linked to a task //! for time-blocking. They support recurrence patterns (Daily, Weekly, Monthly), //! optional project and contact associations, and block-type classification //! (focus, meeting, break, etc.). use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use crate::constants::DAYS_THRESHOLD_SHORT_FORMAT; use crate::id_types::{EventId, UserId, ProjectId, ContactId, TaskId}; use super::shared::{BlockType, Recurrence, RecurrenceRule}; // ============ Event ============ /// A calendar event with optional time-blocking link to a task. /// /// Events can be standalone or linked to a task for time-blocking purposes. /// When linked, the event represents a scheduled time slot for working on the task. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Event { /// Unique identifier. pub id: EventId, /// Owner user ID (internal). #[serde(skip_serializing)] pub user_id: Option, /// Associated project, if any. pub project_id: Option, /// Denormalized project name for display. pub project_name: Option, /// Associated contact, if any. pub contact_id: Option, /// Denormalized contact name for display. pub contact_name: Option, /// Event title. pub title: String, /// Event description/notes. pub description: String, /// When the event starts. pub start_time: DateTime, /// When the event ends (optional for all-day events). pub end_time: Option>, /// Location (physical address or video link). pub location: Option, /// If this is a time-block, the linked task ID. pub linked_task_id: Option, /// Recurrence pattern (legacy, used when recurrence_rule is absent). pub recurrence: Recurrence, /// Rich recurrence configuration (JSON). Takes precedence over `recurrence`. pub recurrence_rule: Option, /// Original event ID if this is a recurrence instance. pub recurrence_parent_id: Option, /// True if this is a virtual instance produced by recurrence expansion (not persisted). #[serde(default)] pub is_recurring_instance: bool, /// If this is a time block, the block type. pub block_type: Option, /// External sync source (e.g., "vcf", "ics", "google", "apple"). pub external_source: Option, /// External ID for dedup (e.g., UID from .ics, provider-specific ID). pub external_id: Option, /// Whether this event is read-only (synced from external calendar). pub is_read_only: bool, /// If set, the event is snoozed (hidden from main views) until this time. pub snoozed_until: Option>, /// Seconds-before-start_time at which to fire desktop reminder notifications. /// `[0, 300, 900]` = at time, 5 minutes before, 15 minutes before. Empty = no reminders. #[serde(default)] pub reminder_offsets_seconds: Vec, } impl Event { /// Returns a formatted time string (e.g., "Jan 15, 14:00" or "Jan 15, 14:00 - 15:30"). pub fn time_formatted(&self) -> String { let start = self.start_time.format("%b %d, %H:%M").to_string(); match &self.end_time { Some(end) => format!("{} - {}", start, end.format("%H:%M")), None => start, } } /// Returns a relative date string: "Past", "Today", "Tomorrow", day name, or "Mon DD". pub fn date_formatted(&self) -> String { let now = Utc::now(); let days = (self.start_time.date_naive() - now.date_naive()).num_days(); if days < 0 { "Past".to_string() } else if days == 0 { "Today".to_string() } else if days == 1 { "Tomorrow".to_string() } else if days < DAYS_THRESHOLD_SHORT_FORMAT { self.start_time.format("%A").to_string() } else { self.start_time.format("%b %d").to_string() } } /// Returns the zero-padded day of the month (e.g., "07", "15"). pub fn day_number(&self) -> String { self.start_time.format("%d").to_string() } /// Returns the start time as a Unix timestamp (seconds since epoch). pub fn timestamp(&self) -> i64 { self.start_time.timestamp() } /// Returns true if the event has a location set. pub fn has_location(&self) -> bool { self.location.is_some() } /// Returns the location string, or an empty string if unset. pub fn location_or_empty(&self) -> &str { self.location.as_deref().unwrap_or("") } /// Returns true if the event is associated with a project. pub fn has_project(&self) -> bool { self.project_name.is_some() } /// Returns the project name, or an empty string if unset. pub fn project_name_or_empty(&self) -> &str { self.project_name.as_deref().unwrap_or("") } /// Returns true if the event has a non-empty description. pub fn has_description(&self) -> bool { !self.description.is_empty() } /// Returns true if the event has a recurrence pattern set (not `None`). pub fn has_recurrence(&self) -> bool { self.recurrence_rule.is_some() || self.recurrence != Recurrence::None } /// Returns the effective recurrence rule, synthesizing from the legacy /// column if no explicit rule is set. pub fn effective_recurrence_rule(&self) -> Option { RecurrenceRule::effective(self.recurrence_rule.as_ref(), &self.recurrence) } /// Returns true if this event is a time-block linked to a task. pub fn is_linked_to_task(&self) -> bool { self.linked_task_id.is_some() } /// True if `snoozed_until` is in the future. pub fn is_snoozed(&self) -> bool { self.snoozed_until.is_some_and(|t| t > Utc::now()) } } // ============ Event DTOs ============ /// Data for creating a new event. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NewEvent { /// Owner user ID (set by the command layer for desktop). pub user_id: Option, /// Associated project, if any. pub project_id: Option, /// Associated contact, if any. pub contact_id: Option, /// Event title (required, validated non-empty). pub title: String, /// Event description or notes. pub description: String, /// When the event starts. pub start_time: DateTime, /// When the event ends (optional for all-day or open-ended events). pub end_time: Option>, /// Location (physical address or video link). pub location: Option, /// Linked task ID for time-blocking (set programmatically, not via form). pub linked_task_id: Option, /// Recurrence pattern (None, Daily, Weekly, Monthly). pub recurrence: Recurrence, /// Rich recurrence configuration (JSON). pub recurrence_rule: Option, /// Block type classification (focus, meeting, break, etc.). pub block_type: Option, /// Seconds-before-start_time at which to fire reminders. Empty = none. #[serde(default)] pub reminder_offsets_seconds: Vec, } /// Data for updating an existing event. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateEvent { /// Associated project, if any. pub project_id: Option, /// Associated contact, if any. pub contact_id: Option, /// Event title (required, validated non-empty). pub title: String, /// Event description or notes. pub description: String, /// When the event starts. pub start_time: DateTime, /// When the event ends (optional for all-day or open-ended events). pub end_time: Option>, /// Location (physical address or video link). pub location: Option, /// Linked task ID for time-blocking (preserved from the existing event on update). pub linked_task_id: Option, /// Recurrence pattern (None, Daily, Weekly, Monthly). pub recurrence: Recurrence, /// Rich recurrence configuration (JSON). pub recurrence_rule: Option, /// Block type classification (focus, meeting, break, etc.). pub block_type: Option, /// Seconds-before-start_time at which to fire reminders. Empty = none. #[serde(default)] pub reminder_offsets_seconds: Vec, } impl NewEvent { /// Creates a builder for constructing a new event. /// /// # Example /// /// ```rust /// use goingson_core::NewEvent; /// use chrono::{Duration, Utc}; /// /// let start = Utc::now(); /// let event = NewEvent::builder("Team Meeting", start) /// .end_time(start + Duration::hours(1)) /// .location("Conference Room A") /// .build(); /// ``` pub fn builder(title: impl Into, start_time: DateTime) -> NewEventBuilder { NewEventBuilder::new(title, start_time) } } /// Builder for constructing [`NewEvent`] with sensible defaults. #[derive(Debug, Clone)] pub struct NewEventBuilder { title: String, start_time: DateTime, user_id: Option, project_id: Option, contact_id: Option, description: String, end_time: Option>, location: Option, linked_task_id: Option, recurrence: Recurrence, recurrence_rule: Option, block_type: Option, } impl NewEventBuilder { /// Creates a new builder with the given title and start time. pub fn new(title: impl Into, start_time: DateTime) -> Self { Self { title: title.into(), start_time, user_id: None, project_id: None, contact_id: None, description: String::new(), end_time: None, location: None, linked_task_id: None, recurrence: Recurrence::default(), recurrence_rule: None, block_type: None, } } /// Sets the user ID. pub fn user_id(mut self, user_id: UserId) -> Self { self.user_id = Some(user_id); self } /// Sets the project ID. pub fn project_id(mut self, project_id: ProjectId) -> Self { self.project_id = Some(project_id); self } /// Sets the contact ID. pub fn contact_id(mut self, contact_id: ContactId) -> Self { self.contact_id = Some(contact_id); self } /// Sets the description. pub fn description(mut self, description: impl Into) -> Self { self.description = description.into(); self } /// Sets the end time. pub fn end_time(mut self, end_time: DateTime) -> Self { self.end_time = Some(end_time); self } /// Sets the location. pub fn location(mut self, location: impl Into) -> Self { self.location = Some(location.into()); self } /// Sets the linked task ID (for time-blocking). pub fn linked_task_id(mut self, task_id: TaskId) -> Self { self.linked_task_id = Some(task_id); self } /// Sets the recurrence pattern. pub fn recurrence(mut self, recurrence: Recurrence) -> Self { self.recurrence = recurrence; self } /// Sets the rich recurrence rule. pub fn recurrence_rule(mut self, rule: RecurrenceRule) -> Self { self.recurrence_rule = Some(rule); self } /// Sets the block type. pub fn block_type(mut self, block_type: BlockType) -> Self { self.block_type = Some(block_type); self } /// Builds the [`NewEvent`]. pub fn build(self) -> NewEvent { NewEvent { user_id: self.user_id, project_id: self.project_id, contact_id: self.contact_id, title: self.title, description: self.description, start_time: self.start_time, end_time: self.end_time, location: self.location, linked_task_id: self.linked_task_id, recurrence: self.recurrence, recurrence_rule: self.recurrence_rule, block_type: self.block_type, reminder_offsets_seconds: Vec::new(), } } }