Complete calendar model refactor to use NaiveDateTime for local time handling

- 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>
This commit is contained in:
Connor Johnstone
2025-09-13 20:57:35 -04:00
parent acc5ced551
commit a6092d13ce
9 changed files with 158 additions and 177 deletions

View File

@@ -1,7 +1,7 @@
//! VEvent - RFC 5545 compliant calendar event structure
use crate::common::*;
use chrono::{DateTime, Duration, Utc};
use chrono::{DateTime, Duration, NaiveDateTime, Utc};
use serde::{Deserialize, Serialize};
// ==================== VEVENT COMPONENT ====================
@@ -9,12 +9,14 @@ use serde::{Deserialize, Serialize};
#[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
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<DateTime<Utc>>, // End date-time (DTEND)
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)
@@ -43,14 +45,19 @@ pub struct VEvent {
// 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)
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<DateTime<Utc>>, // Recurrence dates (RDATE)
pub exdate: Vec<DateTime<Utc>>, // Exception dates (EXDATE)
pub recurrence_id: Option<DateTime<Utc>>, // Recurrence ID (RECURRENCE-ID)
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
@@ -64,13 +71,15 @@ pub struct VEvent {
}
impl VEvent {
/// Create a new VEvent with required fields
pub fn new(uid: String, dtstart: DateTime<Utc>) -> Self {
/// 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,
@@ -89,12 +98,17 @@ impl VEvent {
url: None,
geo: None,
sequence: None,
created: Some(Utc::now()),
last_modified: Some(Utc::now()),
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,
@@ -105,7 +119,7 @@ impl VEvent {
}
/// Helper method to get effective end time (dtend or dtstart + duration)
pub fn get_end_time(&self) -> DateTime<Utc> {
pub fn get_end_time(&self) -> NaiveDateTime {
if let Some(dtend) = self.dtend {
dtend
} else if let Some(duration) = self.duration {
@@ -136,7 +150,7 @@ impl VEvent {
/// Helper method to get start date for UI compatibility
pub fn get_date(&self) -> chrono::NaiveDate {
self.dtstart.date_naive()
self.dtstart.date()
}
/// Check if event is recurring