Complete frontend migration to shared VEvent model
- 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>
This commit is contained in:
		| @@ -1,841 +1,2 @@ | ||||
| // RFC 5545 Compliant iCalendar Data Structures | ||||
| // This file contains updated structures that fully comply with RFC 5545 iCalendar specification | ||||
|  | ||||
| use chrono::{DateTime, Utc, Duration}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| // ==================== CALENDAR OBJECT (VCALENDAR) ==================== | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct ICalendarObject { | ||||
|     // Required calendar properties | ||||
|     pub prodid: String,                    // Product identifier (PRODID) | ||||
|     pub version: String,                   // Version (typically "2.0") | ||||
|      | ||||
|     // Optional calendar properties | ||||
|     pub calscale: Option<String>,          // Calendar scale (CALSCALE) - default "GREGORIAN" | ||||
|     pub method: Option<String>,            // Method (METHOD) | ||||
|      | ||||
|     // Components | ||||
|     pub events: Vec<VEvent>,               // VEVENT components | ||||
|     pub todos: Vec<VTodo>,                 // VTODO components | ||||
|     pub journals: Vec<VJournal>,           // VJOURNAL components | ||||
|     pub freebusys: Vec<VFreeBusy>,         // VFREEBUSY components | ||||
|     pub timezones: Vec<VTimeZone>,         // VTIMEZONE components | ||||
| } | ||||
|  | ||||
| // ==================== 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 | ||||
| } | ||||
|  | ||||
| // ==================== VTODO COMPONENT ==================== | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct VTodo { | ||||
|     // Required properties | ||||
|     pub dtstamp: DateTime<Utc>,            // Date-time stamp (DTSTAMP) - REQUIRED | ||||
|     pub uid: String,                       // Unique identifier (UID) - REQUIRED | ||||
|      | ||||
|     // Optional date-time properties | ||||
|     pub dtstart: Option<DateTime<Utc>>,    // Start date-time (DTSTART) | ||||
|     pub due: Option<DateTime<Utc>>,        // Due date-time (DUE) | ||||
|     pub duration: Option<Duration>,        // Duration (DURATION) | ||||
|     pub completed: Option<DateTime<Utc>>,  // Completion date-time (COMPLETED) | ||||
|      | ||||
|     // Descriptive properties | ||||
|     pub summary: Option<String>,           // Summary/title (SUMMARY) | ||||
|     pub description: Option<String>,       // Description (DESCRIPTION) | ||||
|     pub location: Option<String>,          // Location (LOCATION) | ||||
|      | ||||
|     // Status and completion | ||||
|     pub status: Option<TodoStatus>,        // Status (STATUS) | ||||
|     pub percent_complete: Option<u8>,      // Percent complete 0-100 (PERCENT-COMPLETE) | ||||
|     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) | ||||
| } | ||||
|  | ||||
| // ==================== VJOURNAL COMPONENT ==================== | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct VJournal { | ||||
|     // Required properties | ||||
|     pub dtstamp: DateTime<Utc>,            // Date-time stamp (DTSTAMP) - REQUIRED | ||||
|     pub uid: String,                       // Unique identifier (UID) - REQUIRED | ||||
|      | ||||
|     // Optional properties | ||||
|     pub dtstart: Option<DateTime<Utc>>,    // Start date-time (DTSTART) | ||||
|     pub summary: Option<String>,           // Summary/title (SUMMARY) | ||||
|     pub description: Option<String>,       // Description (DESCRIPTION) | ||||
|      | ||||
|     // Classification and status | ||||
|     pub class: Option<EventClass>,         // Classification (CLASS) | ||||
|     pub status: Option<JournalStatus>,     // Status (STATUS) | ||||
|      | ||||
|     // People and organization | ||||
|     pub organizer: Option<CalendarUser>,   // Organizer (ORGANIZER) | ||||
|     pub contact: Option<String>,           // Contact information (CONTACT) | ||||
|      | ||||
|     // Categorization and relationships | ||||
|     pub categories: Vec<String>,           // Categories (CATEGORIES) | ||||
|     pub comment: Option<String>,           // Comment (COMMENT) | ||||
|     pub related_to: Option<String>,        // Related component (RELATED-TO) | ||||
|     pub url: Option<String>,               // URL (URL) | ||||
|      | ||||
|     // 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) | ||||
|      | ||||
|     // Attachments | ||||
|     pub attachments: Vec<Attachment>,      // Attachments (ATTACH) | ||||
| } | ||||
|  | ||||
| // ==================== VFREEBUSY COMPONENT ==================== | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct VFreeBusy { | ||||
|     // Required properties | ||||
|     pub dtstamp: DateTime<Utc>,            // Date-time stamp (DTSTAMP) - REQUIRED | ||||
|     pub uid: String,                       // Unique identifier (UID) - REQUIRED | ||||
|      | ||||
|     // Optional properties | ||||
|     pub dtstart: Option<DateTime<Utc>>,    // Start date-time (DTSTART) | ||||
|     pub dtend: Option<DateTime<Utc>>,      // End date-time (DTEND) | ||||
|     pub duration: Option<Duration>,        // Duration (DURATION) | ||||
|      | ||||
|     // Free/busy information | ||||
|     pub freebusy: Vec<FreeBusyTime>,       // Free/busy periods (FREEBUSY) | ||||
|      | ||||
|     // People and organization | ||||
|     pub organizer: Option<CalendarUser>,   // Organizer (ORGANIZER) | ||||
|     pub attendees: Vec<CalendarUser>,      // Attendees (ATTENDEE) | ||||
|     pub contact: Option<String>,           // Contact information (CONTACT) | ||||
|      | ||||
|     // Additional properties | ||||
|     pub comment: Option<String>,           // Comment (COMMENT) | ||||
|     pub url: Option<String>,               // URL (URL) | ||||
| } | ||||
|  | ||||
| // ==================== VTIMEZONE COMPONENT ==================== | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct VTimeZone { | ||||
|     // Required properties | ||||
|     pub tzid: String,                      // Time zone identifier (TZID) - REQUIRED | ||||
|      | ||||
|     // Optional properties | ||||
|     pub tzname: Option<String>,            // Time zone name (TZNAME) | ||||
|     pub tzurl: Option<String>,             // Time zone URL (TZURL) | ||||
|      | ||||
|     // Standard and daylight components | ||||
|     pub standard: Vec<TimeZoneComponent>,   // Standard time components | ||||
|     pub daylight: Vec<TimeZoneComponent>,   // Daylight time components | ||||
|      | ||||
|     // Last modified | ||||
|     pub last_modified: Option<DateTime<Utc>>, // Last modified (LAST-MODIFIED) | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct TimeZoneComponent { | ||||
|     pub dtstart: DateTime<Utc>,            // Start date-time (DTSTART) - REQUIRED | ||||
|     pub tzoffsetfrom: String,              // UTC offset from (TZOFFSETFROM) - REQUIRED | ||||
|     pub tzoffsetto: String,                // UTC offset to (TZOFFSETTO) - REQUIRED | ||||
|      | ||||
|     pub tzname: Option<String>,            // Time zone name (TZNAME) | ||||
|     pub comment: Option<String>,           // Comment (COMMENT) | ||||
|      | ||||
|     // Recurrence | ||||
|     pub rrule: Option<String>,             // Recurrence rule (RRULE) | ||||
|     pub rdate: Vec<DateTime<Utc>>,         // Recurrence dates (RDATE) | ||||
| } | ||||
|  | ||||
| // ==================== VALARM COMPONENT ==================== | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct VAlarm { | ||||
|     pub action: AlarmAction,               // Action (ACTION) - REQUIRED | ||||
|     pub trigger: AlarmTrigger,             // Trigger (TRIGGER) - REQUIRED | ||||
|      | ||||
|     // Optional properties (some required based on action) | ||||
|     pub description: Option<String>,       // Description (DESCRIPTION) | ||||
|     pub summary: Option<String>,           // Summary (SUMMARY) | ||||
|     pub duration: Option<Duration>,        // Duration (DURATION) | ||||
|     pub repeat: Option<u32>,               // Repeat count (REPEAT) | ||||
|     pub attendees: Vec<CalendarUser>,      // Attendees (ATTENDEE) - for EMAIL action | ||||
|     pub attachments: Vec<Attachment>,      // Attachments (ATTACH) | ||||
| } | ||||
|  | ||||
| // ==================== SUPPORTING TYPES ==================== | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum EventClass { | ||||
|     Public, | ||||
|     Private, | ||||
|     Confidential, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum EventStatus { | ||||
|     Tentative, | ||||
|     Confirmed, | ||||
|     Cancelled, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum TodoStatus { | ||||
|     NeedsAction, | ||||
|     Completed, | ||||
|     InProcess, | ||||
|     Cancelled, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum JournalStatus { | ||||
|     Draft, | ||||
|     Final, | ||||
|     Cancelled, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum TimeTransparency { | ||||
|     Opaque,      // Time is not available (default) | ||||
|     Transparent, // Time is available despite event | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum AlarmAction { | ||||
|     Audio, | ||||
|     Display, | ||||
|     Email, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum AlarmTrigger { | ||||
|     Duration(Duration),                    // Relative to start/end | ||||
|     DateTime(DateTime<Utc>),              // Absolute time | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct CalendarUser { | ||||
|     pub cal_address: String,               // Calendar address (email) | ||||
|     pub cn: Option<String>,                // Common name (CN parameter) | ||||
|     pub dir: Option<String>,               // Directory entry (DIR parameter) | ||||
|     pub sent_by: Option<String>,           // Sent by (SENT-BY parameter) | ||||
|     pub language: Option<String>,          // Language (LANGUAGE parameter) | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct Attendee { | ||||
|     pub cal_address: String,               // Calendar address (email) | ||||
|     pub cn: Option<String>,                // Common name (CN parameter) | ||||
|     pub role: Option<AttendeeRole>,        // Role (ROLE parameter) | ||||
|     pub partstat: Option<ParticipationStatus>, // Participation status (PARTSTAT parameter) | ||||
|     pub rsvp: Option<bool>,                // RSVP expectation (RSVP parameter) | ||||
|     pub cutype: Option<CalendarUserType>,  // Calendar user type (CUTYPE parameter) | ||||
|     pub member: Vec<String>,               // Group/list membership (MEMBER parameter) | ||||
|     pub delegated_to: Vec<String>,         // Delegated to (DELEGATED-TO parameter) | ||||
|     pub delegated_from: Vec<String>,       // Delegated from (DELEGATED-FROM parameter) | ||||
|     pub sent_by: Option<String>,           // Sent by (SENT-BY parameter) | ||||
|     pub dir: Option<String>,               // Directory entry (DIR parameter) | ||||
|     pub language: Option<String>,          // Language (LANGUAGE parameter) | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum AttendeeRole { | ||||
|     Chair, | ||||
|     ReqParticipant, | ||||
|     OptParticipant, | ||||
|     NonParticipant, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum ParticipationStatus { | ||||
|     NeedsAction, | ||||
|     Accepted, | ||||
|     Declined, | ||||
|     Tentative, | ||||
|     Delegated, | ||||
|     Completed, | ||||
|     InProcess, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum CalendarUserType { | ||||
|     Individual, | ||||
|     Group, | ||||
|     Resource, | ||||
|     Room, | ||||
|     Unknown, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct GeographicPosition { | ||||
|     pub latitude: f64,                     // Latitude in decimal degrees | ||||
|     pub longitude: f64,                    // Longitude in decimal degrees | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct Attachment { | ||||
|     pub data: AttachmentData,              // Attachment data | ||||
|     pub fmttype: Option<String>,           // Format type (FMTTYPE parameter) | ||||
|     pub encoding: Option<String>,          // Encoding (ENCODING parameter) | ||||
|     pub filename: Option<String>,          // Filename (X-FILENAME parameter) | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum AttachmentData { | ||||
|     Uri(String),                           // URI reference | ||||
|     Binary(Vec<u8>),                       // Binary data | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub struct FreeBusyTime { | ||||
|     pub period: (DateTime<Utc>, DateTime<Utc>), // Start and end time | ||||
|     pub fbtype: Option<FreeBusyType>,      // Free/busy type | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] | ||||
| pub enum FreeBusyType { | ||||
|     Free, | ||||
|     Busy, | ||||
|     BusyUnavailable, | ||||
|     BusyTentative, | ||||
| } | ||||
|  | ||||
| // ==================== COMPATIBILITY LAYER ==================== | ||||
|  | ||||
| use crate::services::calendar_service::{CalendarEvent, EventReminder, ReminderAction}; | ||||
|  | ||||
| // Conversion from new VEvent to existing CalendarEvent | ||||
| impl From<VEvent> for CalendarEvent { | ||||
|     fn from(vevent: VEvent) -> Self { | ||||
|         Self { | ||||
|             uid: vevent.uid, | ||||
|             summary: vevent.summary, | ||||
|             description: vevent.description, | ||||
|             start: vevent.dtstart, | ||||
|             end: vevent.dtend, | ||||
|             location: vevent.location, | ||||
|             status: vevent.status.unwrap_or(EventStatus::Confirmed).into(), | ||||
|             class: vevent.class.unwrap_or(EventClass::Public).into(), | ||||
|             priority: vevent.priority, | ||||
|             organizer: vevent.organizer.map(|o| o.cal_address), | ||||
|             attendees: vevent.attendees.into_iter().map(|a| a.cal_address).collect(), | ||||
|             categories: vevent.categories, | ||||
|             created: vevent.created, | ||||
|             last_modified: vevent.last_modified, | ||||
|             recurrence_rule: vevent.rrule, | ||||
|             exception_dates: vevent.exdate, | ||||
|             all_day: vevent.all_day, | ||||
|             reminders: vevent.alarms.into_iter().map(|a| a.into()).collect(), | ||||
|             etag: vevent.etag, | ||||
|             href: vevent.href, | ||||
|             calendar_path: vevent.calendar_path, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Conversion from existing CalendarEvent to new VEvent | ||||
| impl From<CalendarEvent> for VEvent { | ||||
|     fn from(event: CalendarEvent) -> Self { | ||||
|         use chrono::Utc; | ||||
|          | ||||
|         Self { | ||||
|             // Required properties | ||||
|             dtstamp: Utc::now(), // Add required DTSTAMP | ||||
|             uid: event.uid, | ||||
|             dtstart: event.start, | ||||
|              | ||||
|             // Optional properties | ||||
|             dtend: event.end, | ||||
|             duration: None, // Will be calculated from dtend if needed | ||||
|             summary: event.summary, | ||||
|             description: event.description, | ||||
|             location: event.location, | ||||
|              | ||||
|             // Classification and status | ||||
|             class: Some(event.class.into()), | ||||
|             status: Some(event.status.into()), | ||||
|             transp: None, // Default to None, can be enhanced later | ||||
|             priority: event.priority, | ||||
|              | ||||
|             // People and organization | ||||
|             organizer: event.organizer.map(|email| CalendarUser { | ||||
|                 cal_address: email, | ||||
|                 cn: None, | ||||
|                 dir: None, | ||||
|                 sent_by: None, | ||||
|                 language: None, | ||||
|             }), | ||||
|             attendees: event.attendees.into_iter().map(|email| Attendee { | ||||
|                 cal_address: email, | ||||
|                 cn: None, | ||||
|                 role: None, | ||||
|                 partstat: None, | ||||
|                 rsvp: None, | ||||
|                 cutype: None, | ||||
|                 member: Vec::new(), | ||||
|                 delegated_to: Vec::new(), | ||||
|                 delegated_from: Vec::new(), | ||||
|                 sent_by: None, | ||||
|                 dir: None, | ||||
|                 language: None, | ||||
|             }).collect(), | ||||
|             contact: None, | ||||
|              | ||||
|             // Categorization and relationships | ||||
|             categories: event.categories, | ||||
|             comment: None, | ||||
|             resources: Vec::new(), | ||||
|             related_to: None, | ||||
|             url: None, | ||||
|              | ||||
|             // Geographical | ||||
|             geo: None, | ||||
|              | ||||
|             // Versioning and modification | ||||
|             sequence: Some(0), // Start with sequence 0 | ||||
|             created: event.created, | ||||
|             last_modified: event.last_modified, | ||||
|              | ||||
|             // Recurrence | ||||
|             rrule: event.recurrence_rule, | ||||
|             rdate: Vec::new(), | ||||
|             exdate: event.exception_dates, | ||||
|             recurrence_id: None, | ||||
|              | ||||
|             // Alarms and attachments | ||||
|             alarms: event.reminders.into_iter().map(|r| r.into()).collect(), | ||||
|             attachments: Vec::new(), | ||||
|              | ||||
|             // CalDAV specific | ||||
|             etag: event.etag, | ||||
|             href: event.href, | ||||
|             calendar_path: event.calendar_path, | ||||
|             all_day: event.all_day, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Convert between status enums | ||||
| impl From<EventStatus> for crate::services::calendar_service::EventStatus { | ||||
|     fn from(status: EventStatus) -> Self { | ||||
|         match status { | ||||
|             EventStatus::Tentative => crate::services::calendar_service::EventStatus::Tentative, | ||||
|             EventStatus::Confirmed => crate::services::calendar_service::EventStatus::Confirmed, | ||||
|             EventStatus::Cancelled => crate::services::calendar_service::EventStatus::Cancelled, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<crate::services::calendar_service::EventStatus> for EventStatus { | ||||
|     fn from(status: crate::services::calendar_service::EventStatus) -> Self { | ||||
|         match status { | ||||
|             crate::services::calendar_service::EventStatus::Tentative => EventStatus::Tentative, | ||||
|             crate::services::calendar_service::EventStatus::Confirmed => EventStatus::Confirmed, | ||||
|             crate::services::calendar_service::EventStatus::Cancelled => EventStatus::Cancelled, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Convert between class enums | ||||
| impl From<EventClass> for crate::services::calendar_service::EventClass { | ||||
|     fn from(class: EventClass) -> Self { | ||||
|         match class { | ||||
|             EventClass::Public => crate::services::calendar_service::EventClass::Public, | ||||
|             EventClass::Private => crate::services::calendar_service::EventClass::Private, | ||||
|             EventClass::Confidential => crate::services::calendar_service::EventClass::Confidential, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<crate::services::calendar_service::EventClass> for EventClass { | ||||
|     fn from(class: crate::services::calendar_service::EventClass) -> Self { | ||||
|         match class { | ||||
|             crate::services::calendar_service::EventClass::Public => EventClass::Public, | ||||
|             crate::services::calendar_service::EventClass::Private => EventClass::Private, | ||||
|             crate::services::calendar_service::EventClass::Confidential => EventClass::Confidential, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Convert between reminder types | ||||
| impl From<VAlarm> for EventReminder { | ||||
|     fn from(alarm: VAlarm) -> Self { | ||||
|         let minutes_before = match alarm.trigger { | ||||
|             AlarmTrigger::Duration(duration) => { | ||||
|                 // Convert duration to minutes (assuming it's negative for "before") | ||||
|                 (-duration.num_minutes()) as i32 | ||||
|             }, | ||||
|             AlarmTrigger::DateTime(_) => 0, // Absolute time alarms default to 0 minutes | ||||
|         }; | ||||
|          | ||||
|         let action = match alarm.action { | ||||
|             AlarmAction::Display => ReminderAction::Display, | ||||
|             AlarmAction::Audio => ReminderAction::Audio, | ||||
|             AlarmAction::Email => ReminderAction::Email, | ||||
|         }; | ||||
|          | ||||
|         Self { | ||||
|             minutes_before, | ||||
|             action, | ||||
|             description: alarm.description, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<EventReminder> for VAlarm { | ||||
|     fn from(reminder: EventReminder) -> Self { | ||||
|         use chrono::Duration; | ||||
|          | ||||
|         let action = match reminder.action { | ||||
|             ReminderAction::Display => AlarmAction::Display, | ||||
|             ReminderAction::Audio => AlarmAction::Audio, | ||||
|             ReminderAction::Email => AlarmAction::Email, | ||||
|         }; | ||||
|          | ||||
|         let trigger = AlarmTrigger::Duration(Duration::minutes(-reminder.minutes_before as i64)); | ||||
|          | ||||
|         Self { | ||||
|             action, | ||||
|             trigger, | ||||
|             description: reminder.description, | ||||
|             summary: None, | ||||
|             duration: None, | ||||
|             repeat: None, | ||||
|             attendees: Vec::new(), | ||||
|             attachments: Vec::new(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // ==================== CONVERSION FUNCTIONS ==================== | ||||
|  | ||||
| impl VEvent { | ||||
|     /// Convert from legacy CalendarEvent to RFC 5545 compliant VEvent | ||||
|     pub fn from_calendar_event(event: &crate::services::calendar_service::CalendarEvent) -> Self { | ||||
|         let now = chrono::Utc::now(); | ||||
|          | ||||
|         // Convert attendees from strings to structured Attendee objects | ||||
|         let attendees = event.attendees.iter() | ||||
|             .map(|email| Attendee { | ||||
|                 cal_address: email.clone(), | ||||
|                 cn: None, | ||||
|                 role: Some(AttendeeRole::ReqParticipant), | ||||
|                 partstat: Some(ParticipationStatus::NeedsAction), | ||||
|                 rsvp: Some(true), | ||||
|                 cutype: Some(CalendarUserType::Individual), | ||||
|                 member: Vec::new(), | ||||
|                 delegated_to: Vec::new(), | ||||
|                 delegated_from: Vec::new(), | ||||
|                 sent_by: None, | ||||
|                 dir: None, | ||||
|                 language: None, | ||||
|             }) | ||||
|             .collect(); | ||||
|  | ||||
|         // Convert reminders to alarms | ||||
|         let alarms = event.reminders.iter() | ||||
|             .map(|reminder| { | ||||
|                 let action = match reminder.action { | ||||
|                     crate::services::calendar_service::ReminderAction::Display => AlarmAction::Display, | ||||
|                     crate::services::calendar_service::ReminderAction::Audio => AlarmAction::Audio, | ||||
|                     crate::services::calendar_service::ReminderAction::Email => AlarmAction::Email, | ||||
|                 }; | ||||
|                  | ||||
|                 VAlarm { | ||||
|                     action, | ||||
|                     trigger: AlarmTrigger::Duration(chrono::Duration::minutes(-reminder.minutes_before as i64)), | ||||
|                     description: reminder.description.clone(), | ||||
|                     summary: None, | ||||
|                     duration: None, | ||||
|                     repeat: None, | ||||
|                     attendees: Vec::new(), | ||||
|                     attachments: Vec::new(), | ||||
|                 } | ||||
|             }) | ||||
|             .collect(); | ||||
|  | ||||
|         // Convert status | ||||
|         let status = match event.status { | ||||
|             crate::services::calendar_service::EventStatus::Tentative => Some(EventStatus::Tentative), | ||||
|             crate::services::calendar_service::EventStatus::Confirmed => Some(EventStatus::Confirmed), | ||||
|             crate::services::calendar_service::EventStatus::Cancelled => Some(EventStatus::Cancelled), | ||||
|         }; | ||||
|  | ||||
|         // Convert class | ||||
|         let class = match event.class { | ||||
|             crate::services::calendar_service::EventClass::Public => Some(EventClass::Public), | ||||
|             crate::services::calendar_service::EventClass::Private => Some(EventClass::Private), | ||||
|             crate::services::calendar_service::EventClass::Confidential => Some(EventClass::Confidential), | ||||
|         }; | ||||
|  | ||||
|         Self { | ||||
|             // Required properties | ||||
|             dtstamp: event.last_modified.unwrap_or(now), | ||||
|             uid: event.uid.clone(), | ||||
|             dtstart: event.start, | ||||
|              | ||||
|             // Date/time properties | ||||
|             dtend: event.end, | ||||
|             duration: None, // Will use dtend instead | ||||
|              | ||||
|             // Content properties | ||||
|             summary: event.summary.clone(), | ||||
|             description: event.description.clone(), | ||||
|             location: event.location.clone(), | ||||
|              | ||||
|             // Classification and status | ||||
|             class, | ||||
|             status, | ||||
|             transp: None, // Default transparency | ||||
|             priority: event.priority, | ||||
|              | ||||
|             // People and organization | ||||
|             organizer: event.organizer.as_ref().map(|org| CalendarUser { | ||||
|                 cal_address: org.clone(), | ||||
|                 cn: None, | ||||
|                 dir: None, | ||||
|                 sent_by: None, | ||||
|                 language: None, | ||||
|             }), | ||||
|             attendees, | ||||
|             contact: None, | ||||
|              | ||||
|             // Categorization | ||||
|             categories: event.categories.clone(), | ||||
|             comment: None, | ||||
|             resources: Vec::new(), | ||||
|             related_to: None, | ||||
|             url: None, | ||||
|              | ||||
|             // Geographic | ||||
|             geo: None, | ||||
|              | ||||
|             // Versioning | ||||
|             sequence: Some(0), // Start with sequence 0 | ||||
|             created: event.created, | ||||
|             last_modified: event.last_modified, | ||||
|              | ||||
|             // Recurrence | ||||
|             rrule: event.recurrence_rule.clone(), | ||||
|             rdate: Vec::new(), | ||||
|             exdate: event.exception_dates.clone(), | ||||
|             recurrence_id: None, | ||||
|              | ||||
|             // Alarms and attachments | ||||
|             alarms, | ||||
|             attachments: Vec::new(), | ||||
|              | ||||
|             // CalDAV specific | ||||
|             etag: event.etag.clone(), | ||||
|             href: event.href.clone(), | ||||
|             calendar_path: event.calendar_path.clone(), | ||||
|             all_day: event.all_day, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Convert to legacy CalendarEvent for backward compatibility | ||||
|     #[allow(dead_code)] | ||||
|     pub fn to_calendar_event(&self) -> crate::services::calendar_service::CalendarEvent { | ||||
|         // Convert attendees back to simple email strings | ||||
|         let attendees = self.attendees.iter() | ||||
|             .map(|attendee| attendee.cal_address.clone()) | ||||
|             .collect(); | ||||
|  | ||||
|         // Convert alarms back to simple reminders | ||||
|         let reminders = self.alarms.iter() | ||||
|             .filter_map(|alarm| { | ||||
|                 if let AlarmTrigger::Duration(duration) = &alarm.trigger { | ||||
|                     Some(crate::services::calendar_service::EventReminder { | ||||
|                         minutes_before: (-duration.num_minutes()) as i32, | ||||
|                         action: match alarm.action { | ||||
|                             AlarmAction::Display => crate::services::calendar_service::ReminderAction::Display, | ||||
|                             AlarmAction::Audio => crate::services::calendar_service::ReminderAction::Audio, | ||||
|                             AlarmAction::Email => crate::services::calendar_service::ReminderAction::Email, | ||||
|                         }, | ||||
|                         description: alarm.description.clone(), | ||||
|                     }) | ||||
|                 } else { | ||||
|                     None | ||||
|                 } | ||||
|             }) | ||||
|             .collect(); | ||||
|  | ||||
|         // Convert status | ||||
|         let status = match self.status { | ||||
|             Some(EventStatus::Tentative) => crate::services::calendar_service::EventStatus::Tentative, | ||||
|             Some(EventStatus::Cancelled) => crate::services::calendar_service::EventStatus::Cancelled, | ||||
|             _ => crate::services::calendar_service::EventStatus::Confirmed, | ||||
|         }; | ||||
|  | ||||
|         // Convert class | ||||
|         let class = match self.class { | ||||
|             Some(EventClass::Private) => crate::services::calendar_service::EventClass::Private, | ||||
|             Some(EventClass::Confidential) => crate::services::calendar_service::EventClass::Confidential, | ||||
|             _ => crate::services::calendar_service::EventClass::Public, | ||||
|         }; | ||||
|  | ||||
|         crate::services::calendar_service::CalendarEvent { | ||||
|             uid: self.uid.clone(), | ||||
|             summary: self.summary.clone(), | ||||
|             description: self.description.clone(), | ||||
|             start: self.dtstart, | ||||
|             end: self.dtend, | ||||
|             location: self.location.clone(), | ||||
|             status, | ||||
|             class, | ||||
|             priority: self.priority, | ||||
|             organizer: self.organizer.as_ref().map(|org| org.cal_address.clone()), | ||||
|             attendees, | ||||
|             categories: self.categories.clone(), | ||||
|             created: self.created, | ||||
|             last_modified: self.last_modified, | ||||
|             recurrence_rule: self.rrule.clone(), | ||||
|             exception_dates: self.exdate.clone(), | ||||
|             all_day: self.all_day, | ||||
|             reminders, | ||||
|             etag: self.etag.clone(), | ||||
|             href: self.href.clone(), | ||||
|             calendar_path: self.calendar_path.clone(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Get the date for this event (for calendar display) | ||||
|     pub fn get_date(&self) -> chrono::NaiveDate { | ||||
|         if self.all_day { | ||||
|             self.dtstart.date_naive() | ||||
|         } else { | ||||
|             self.dtstart.date_naive() | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /// Get display title for the event | ||||
|     pub fn get_title(&self) -> String { | ||||
|         self.summary.clone().unwrap_or_else(|| "Untitled Event".to_string()) | ||||
|     } | ||||
|      | ||||
|     /// 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), | ||||
|         } | ||||
|     } | ||||
| } | ||||
| // Re-export from shared calendar-models library for backward compatibility | ||||
| pub use calendar_models::*; | ||||
		Reference in New Issue
	
	Block a user
	 Connor Johnstone
					Connor Johnstone