//! VEvent - RFC 5545 compliant calendar event structure use crate::common::*; use chrono::{DateTime, Duration, NaiveDateTime, Utc}; use serde::{Deserialize, Serialize}; // ==================== VEVENT COMPONENT ==================== #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct VEvent { // Required properties pub dtstamp: DateTime, // Date-time stamp (DTSTAMP) - REQUIRED (always UTC) pub uid: String, // Unique identifier (UID) - REQUIRED pub dtstart: NaiveDateTime, // Start date-time (DTSTART) - REQUIRED (local time) pub dtstart_tzid: Option, // Timezone ID for DTSTART (TZID parameter) // Optional properties (commonly used) pub dtend: Option, // End date-time (DTEND) (local time) pub dtend_tzid: Option, // Timezone ID for DTEND (TZID parameter) pub duration: Option, // Duration (DURATION) - alternative to DTEND pub summary: Option, // Summary/title (SUMMARY) pub description: Option, // Description (DESCRIPTION) pub location: Option, // Location (LOCATION) // Classification and status pub class: Option, // Classification (CLASS) pub status: Option, // Status (STATUS) pub transp: Option, // Time transparency (TRANSP) pub priority: Option, // Priority 0-9 (PRIORITY) // People and organization pub organizer: Option, // Organizer (ORGANIZER) pub attendees: Vec, // Attendees (ATTENDEE) pub contact: Option, // Contact information (CONTACT) // Categorization and relationships pub categories: Vec, // Categories (CATEGORIES) pub comment: Option, // Comment (COMMENT) pub resources: Vec, // Resources (RESOURCES) pub related_to: Option, // Related component (RELATED-TO) pub url: Option, // URL (URL) // Geographical pub geo: Option, // Geographic position (GEO) // Versioning and modification pub sequence: Option, // Sequence number (SEQUENCE) pub created: Option, // Creation time (CREATED) (local time) pub created_tzid: Option, // Timezone ID for CREATED pub last_modified: Option, // Last modified (LAST-MODIFIED) (local time) pub last_modified_tzid: Option, // Timezone ID for LAST-MODIFIED // Recurrence pub rrule: Option, // Recurrence rule (RRULE) pub rdate: Vec, // Recurrence dates (RDATE) (local time) pub rdate_tzid: Option, // Timezone ID for RDATE pub exdate: Vec, // Exception dates (EXDATE) (local time) pub exdate_tzid: Option, // Timezone ID for EXDATE pub recurrence_id: Option, // Recurrence ID (RECURRENCE-ID) (local time) pub recurrence_id_tzid: Option, // Timezone ID for RECURRENCE-ID // Alarms and attachments pub alarms: Vec, // VALARM components pub attachments: Vec, // Attachments (ATTACH) // CalDAV specific (for implementation) pub etag: Option, // ETag for CalDAV pub href: Option, // Href for CalDAV pub calendar_path: Option, // Calendar path pub all_day: bool, // All-day event flag } impl VEvent { /// Create a new VEvent with required fields (local time) pub fn new(uid: String, dtstart: NaiveDateTime) -> Self { Self { dtstamp: Utc::now(), uid, dtstart, dtstart_tzid: None, dtend: None, dtend_tzid: None, duration: None, summary: None, description: None, location: None, class: None, status: None, transp: None, priority: None, organizer: None, attendees: Vec::new(), contact: None, categories: Vec::new(), comment: None, resources: Vec::new(), related_to: None, url: None, geo: None, sequence: None, created: Some(chrono::Local::now().naive_local()), created_tzid: None, last_modified: Some(chrono::Local::now().naive_local()), last_modified_tzid: None, rrule: None, rdate: Vec::new(), rdate_tzid: None, exdate: Vec::new(), exdate_tzid: None, recurrence_id: None, recurrence_id_tzid: None, alarms: Vec::new(), attachments: Vec::new(), etag: None, href: None, calendar_path: None, all_day: false, } } /// Helper method to get effective end time (dtend or dtstart + duration) pub fn get_end_time(&self) -> NaiveDateTime { if let Some(dtend) = self.dtend { dtend } else if let Some(duration) = self.duration { self.dtstart + duration } else { // Default to 1 hour if no end or duration specified self.dtstart + Duration::hours(1) } } /// Helper method to get event duration pub fn get_duration(&self) -> Duration { if let Some(duration) = self.duration { duration } else if let Some(dtend) = self.dtend { dtend - self.dtstart } else { Duration::hours(1) // Default duration } } /// Helper method to get display title (summary or "Untitled Event") pub fn get_title(&self) -> String { self.summary .clone() .unwrap_or_else(|| "Untitled Event".to_string()) } /// Helper method to get start date for UI compatibility pub fn get_date(&self) -> chrono::NaiveDate { self.dtstart.date() } /// Check if event is recurring pub fn is_recurring(&self) -> bool { self.rrule.is_some() } /// Check if this is an exception to a recurring series pub fn is_exception(&self) -> bool { self.recurrence_id.is_some() } /// Get display string for status pub fn get_status_display(&self) -> &'static str { match &self.status { Some(EventStatus::Tentative) => "Tentative", Some(EventStatus::Confirmed) => "Confirmed", Some(EventStatus::Cancelled) => "Cancelled", None => "Confirmed", // Default } } /// Get display string for class pub fn get_class_display(&self) -> &'static str { match &self.class { Some(EventClass::Public) => "Public", Some(EventClass::Private) => "Private", Some(EventClass::Confidential) => "Confidential", None => "Public", // Default } } /// Get display string for priority pub fn get_priority_display(&self) -> String { match self.priority { None => "Not set".to_string(), Some(0) => "Undefined".to_string(), Some(1) => "High".to_string(), Some(p) if p <= 4 => "High".to_string(), Some(5) => "Medium".to_string(), Some(p) if p <= 8 => "Low".to_string(), Some(9) => "Low".to_string(), Some(p) => format!("Priority {}", p), } } }