- Refactor VEvent to use NaiveDateTime for all date/time fields (dtstart, dtend, created, etc.) - Add separate timezone ID fields (_tzid) for proper RFC 5545 compliance - Update all handlers and services to work with naive local times - Fix external calendar event conversion to handle new model structure - Remove UTC conversions from frontend - backend now handles timezone conversion - Update series operations to work with local time throughout the system - Maintain compatibility with existing CalDAV servers and RFC 5545 specification This major refactor simplifies timezone handling by treating all event times as local until the final CalDAV conversion step, eliminating multiple conversion points that caused timing inconsistencies. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
200 lines
7.5 KiB
Rust
200 lines
7.5 KiB
Rust
//! 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<Utc>, // 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<String>, // Timezone ID for DTSTART (TZID parameter)
|
|
|
|
// Optional properties (commonly used)
|
|
pub dtend: Option<NaiveDateTime>, // End date-time (DTEND) (local time)
|
|
pub dtend_tzid: Option<String>, // Timezone ID for DTEND (TZID parameter)
|
|
pub duration: Option<Duration>, // Duration (DURATION) - alternative to DTEND
|
|
pub summary: Option<String>, // Summary/title (SUMMARY)
|
|
pub description: Option<String>, // Description (DESCRIPTION)
|
|
pub location: Option<String>, // Location (LOCATION)
|
|
|
|
// Classification and status
|
|
pub class: Option<EventClass>, // Classification (CLASS)
|
|
pub status: Option<EventStatus>, // Status (STATUS)
|
|
pub transp: Option<TimeTransparency>, // Time transparency (TRANSP)
|
|
pub priority: Option<u8>, // Priority 0-9 (PRIORITY)
|
|
|
|
// People and organization
|
|
pub organizer: Option<CalendarUser>, // Organizer (ORGANIZER)
|
|
pub attendees: Vec<Attendee>, // Attendees (ATTENDEE)
|
|
pub contact: Option<String>, // Contact information (CONTACT)
|
|
|
|
// Categorization and relationships
|
|
pub categories: Vec<String>, // Categories (CATEGORIES)
|
|
pub comment: Option<String>, // Comment (COMMENT)
|
|
pub resources: Vec<String>, // Resources (RESOURCES)
|
|
pub related_to: Option<String>, // Related component (RELATED-TO)
|
|
pub url: Option<String>, // URL (URL)
|
|
|
|
// Geographical
|
|
pub geo: Option<GeographicPosition>, // Geographic position (GEO)
|
|
|
|
// Versioning and modification
|
|
pub sequence: Option<u32>, // Sequence number (SEQUENCE)
|
|
pub created: Option<NaiveDateTime>, // Creation time (CREATED) (local time)
|
|
pub created_tzid: Option<String>, // Timezone ID for CREATED
|
|
pub last_modified: Option<NaiveDateTime>, // Last modified (LAST-MODIFIED) (local time)
|
|
pub last_modified_tzid: Option<String>, // Timezone ID for LAST-MODIFIED
|
|
|
|
// Recurrence
|
|
pub rrule: Option<String>, // Recurrence rule (RRULE)
|
|
pub rdate: Vec<NaiveDateTime>, // Recurrence dates (RDATE) (local time)
|
|
pub rdate_tzid: Option<String>, // Timezone ID for RDATE
|
|
pub exdate: Vec<NaiveDateTime>, // Exception dates (EXDATE) (local time)
|
|
pub exdate_tzid: Option<String>, // Timezone ID for EXDATE
|
|
pub recurrence_id: Option<NaiveDateTime>, // Recurrence ID (RECURRENCE-ID) (local time)
|
|
pub recurrence_id_tzid: Option<String>, // Timezone ID for RECURRENCE-ID
|
|
|
|
// Alarms and attachments
|
|
pub alarms: Vec<VAlarm>, // VALARM components
|
|
pub attachments: Vec<Attachment>, // Attachments (ATTACH)
|
|
|
|
// CalDAV specific (for implementation)
|
|
pub etag: Option<String>, // ETag for CalDAV
|
|
pub href: Option<String>, // Href for CalDAV
|
|
pub calendar_path: Option<String>, // 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),
|
|
}
|
|
}
|
|
}
|