- Update frontend to use shared calendar-models library - Add display methods to VEvent for UI compatibility - Remove duplicate VEvent definitions in frontend - Fix JSON field name mismatch (dtstart/dtend vs start/end) - Create CalendarEvent type alias for backward compatibility - Resolve "missing field start" parsing error 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
183 lines
6.8 KiB
Rust
183 lines
6.8 KiB
Rust
//! VEvent - RFC 5545 compliant calendar event structure
|
|
|
|
use chrono::{DateTime, Utc, Duration};
|
|
use serde::{Deserialize, Serialize};
|
|
use crate::common::*;
|
|
|
|
// ==================== VEVENT COMPONENT ====================
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
pub struct VEvent {
|
|
// Required properties
|
|
pub dtstamp: DateTime<Utc>, // Date-time stamp (DTSTAMP) - REQUIRED
|
|
pub uid: String, // Unique identifier (UID) - REQUIRED
|
|
pub dtstart: DateTime<Utc>, // Start date-time (DTSTART) - REQUIRED
|
|
|
|
// Optional properties (commonly used)
|
|
pub dtend: Option<DateTime<Utc>>, // End date-time (DTEND)
|
|
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<DateTime<Utc>>, // Creation time (CREATED)
|
|
pub last_modified: Option<DateTime<Utc>>, // Last modified (LAST-MODIFIED)
|
|
|
|
// Recurrence
|
|
pub rrule: Option<String>, // Recurrence rule (RRULE)
|
|
pub rdate: Vec<DateTime<Utc>>, // Recurrence dates (RDATE)
|
|
pub exdate: Vec<DateTime<Utc>>, // Exception dates (EXDATE)
|
|
pub recurrence_id: Option<DateTime<Utc>>, // Recurrence ID (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
|
|
pub fn new(uid: String, dtstart: DateTime<Utc>) -> Self {
|
|
Self {
|
|
dtstamp: Utc::now(),
|
|
uid,
|
|
dtstart,
|
|
dtend: 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(Utc::now()),
|
|
last_modified: Some(Utc::now()),
|
|
rrule: None,
|
|
rdate: Vec::new(),
|
|
exdate: Vec::new(),
|
|
recurrence_id: 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) -> DateTime<Utc> {
|
|
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_naive()
|
|
}
|
|
|
|
/// 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),
|
|
}
|
|
}
|
|
} |