Add comprehensive reminder/alarm support to calendar events
- Backend: Parse VALARM components from CalDAV iCalendar data - Backend: Add EventReminder struct with minutes_before, action, and description - Backend: Support Display, Email, and Audio reminder types - Backend: Parse ISO 8601 duration triggers (-PT15M, -P1D, etc.) - Frontend: Add reminders field to CalendarEvent structure - Frontend: Display reminders in event modal with human-readable formatting - Frontend: Show reminder timing (15 minutes before, 1 day before) and action type - Fix: Update Trunk.toml to properly copy CSS files to dist directory 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,14 @@ dist = "dist"
|
|||||||
BACKEND_API_URL = "http://localhost:3000/api"
|
BACKEND_API_URL = "http://localhost:3000/api"
|
||||||
|
|
||||||
[watch]
|
[watch]
|
||||||
watch = ["src", "Cargo.toml"]
|
watch = ["src", "Cargo.toml", "styles.css", "index.html"]
|
||||||
ignore = ["backend/"]
|
ignore = ["backend/"]
|
||||||
|
|
||||||
[serve]
|
[serve]
|
||||||
address = "127.0.0.1"
|
address = "127.0.0.1"
|
||||||
port = 8080
|
port = 8080
|
||||||
open = false
|
open = false
|
||||||
|
|
||||||
|
[[copy]]
|
||||||
|
from = "styles.css"
|
||||||
|
to = "dist/"
|
||||||
BIN
backend/calendar.db
Normal file
BIN
backend/calendar.db
Normal file
Binary file not shown.
@@ -53,6 +53,9 @@ pub struct CalendarEvent {
|
|||||||
/// All-day event flag
|
/// All-day event flag
|
||||||
pub all_day: bool,
|
pub all_day: bool,
|
||||||
|
|
||||||
|
/// Reminders/alarms for this event
|
||||||
|
pub reminders: Vec<EventReminder>,
|
||||||
|
|
||||||
/// ETag from CalDAV server for conflict detection
|
/// ETag from CalDAV server for conflict detection
|
||||||
pub etag: Option<String>,
|
pub etag: Option<String>,
|
||||||
|
|
||||||
@@ -88,6 +91,27 @@ impl Default for EventClass {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Event reminder/alarm information
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct EventReminder {
|
||||||
|
/// How long before the event to trigger the reminder (in minutes)
|
||||||
|
pub minutes_before: i32,
|
||||||
|
|
||||||
|
/// Type of reminder action
|
||||||
|
pub action: ReminderAction,
|
||||||
|
|
||||||
|
/// Optional description for the reminder
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reminder action types
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum ReminderAction {
|
||||||
|
Display,
|
||||||
|
Email,
|
||||||
|
Audio,
|
||||||
|
}
|
||||||
|
|
||||||
/// CalDAV client for fetching and parsing calendar events
|
/// CalDAV client for fetching and parsing calendar events
|
||||||
pub struct CalDAVClient {
|
pub struct CalDAVClient {
|
||||||
config: crate::config::CalDAVConfig,
|
config: crate::config::CalDAVConfig,
|
||||||
@@ -244,8 +268,8 @@ impl CalDAVClient {
|
|||||||
let mut properties: HashMap<String, String> = HashMap::new();
|
let mut properties: HashMap<String, String> = HashMap::new();
|
||||||
|
|
||||||
// Extract all properties from the event
|
// Extract all properties from the event
|
||||||
for property in event.properties {
|
for property in &event.properties {
|
||||||
properties.insert(property.name.to_uppercase(), property.value.unwrap_or_default());
|
properties.insert(property.name.to_uppercase(), property.value.clone().unwrap_or_default());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required UID field
|
// Required UID field
|
||||||
@@ -325,11 +349,82 @@ impl CalDAVClient {
|
|||||||
last_modified,
|
last_modified,
|
||||||
recurrence_rule: properties.get("RRULE").cloned(),
|
recurrence_rule: properties.get("RRULE").cloned(),
|
||||||
all_day,
|
all_day,
|
||||||
|
reminders: self.parse_alarms(&event)?,
|
||||||
etag: None, // Set by caller
|
etag: None, // Set by caller
|
||||||
href: None, // Set by caller
|
href: None, // Set by caller
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse VALARM components from an iCal event
|
||||||
|
fn parse_alarms(&self, event: &ical::parser::ical::component::IcalEvent) -> Result<Vec<EventReminder>, CalDAVError> {
|
||||||
|
let mut reminders = Vec::new();
|
||||||
|
|
||||||
|
for alarm in &event.alarms {
|
||||||
|
if let Ok(reminder) = self.parse_single_alarm(alarm) {
|
||||||
|
reminders.push(reminder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(reminders)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a single VALARM component into an EventReminder
|
||||||
|
fn parse_single_alarm(&self, alarm: &ical::parser::ical::component::IcalAlarm) -> Result<EventReminder, CalDAVError> {
|
||||||
|
let mut properties: HashMap<String, String> = HashMap::new();
|
||||||
|
|
||||||
|
// Extract all properties from the alarm
|
||||||
|
for property in &alarm.properties {
|
||||||
|
properties.insert(property.name.to_uppercase(), property.value.clone().unwrap_or_default());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse ACTION (required)
|
||||||
|
let action = match properties.get("ACTION").map(|s| s.to_uppercase()) {
|
||||||
|
Some(ref action_str) if action_str == "DISPLAY" => ReminderAction::Display,
|
||||||
|
Some(ref action_str) if action_str == "EMAIL" => ReminderAction::Email,
|
||||||
|
Some(ref action_str) if action_str == "AUDIO" => ReminderAction::Audio,
|
||||||
|
_ => ReminderAction::Display, // Default
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse TRIGGER (required)
|
||||||
|
let minutes_before = if let Some(trigger) = properties.get("TRIGGER") {
|
||||||
|
self.parse_trigger_duration(trigger).unwrap_or(15) // Default 15 minutes
|
||||||
|
} else {
|
||||||
|
15 // Default 15 minutes
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get description
|
||||||
|
let description = properties.get("DESCRIPTION").cloned();
|
||||||
|
|
||||||
|
Ok(EventReminder {
|
||||||
|
minutes_before,
|
||||||
|
action,
|
||||||
|
description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a TRIGGER duration string into minutes before event
|
||||||
|
fn parse_trigger_duration(&self, trigger: &str) -> Option<i32> {
|
||||||
|
// Basic parsing of ISO 8601 duration or relative time
|
||||||
|
// Examples: "-PT15M" (15 minutes before), "-P1D" (1 day before)
|
||||||
|
|
||||||
|
if trigger.starts_with("-PT") && trigger.ends_with("M") {
|
||||||
|
// Parse "-PT15M" format (minutes)
|
||||||
|
let minutes_str = &trigger[3..trigger.len()-1];
|
||||||
|
minutes_str.parse::<i32>().ok()
|
||||||
|
} else if trigger.starts_with("-PT") && trigger.ends_with("H") {
|
||||||
|
// Parse "-PT1H" format (hours)
|
||||||
|
let hours_str = &trigger[3..trigger.len()-1];
|
||||||
|
hours_str.parse::<i32>().ok().map(|h| h * 60)
|
||||||
|
} else if trigger.starts_with("-P") && trigger.ends_with("D") {
|
||||||
|
// Parse "-P1D" format (days)
|
||||||
|
let days_str = &trigger[2..trigger.len()-1];
|
||||||
|
days_str.parse::<i32>().ok().map(|d| d * 24 * 60)
|
||||||
|
} else {
|
||||||
|
// Try to parse as raw minutes
|
||||||
|
trigger.parse::<i32>().ok().map(|m| m.abs())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Discover available calendar collections on the server
|
/// Discover available calendar collections on the server
|
||||||
pub async fn discover_calendars(&self) -> Result<Vec<String>, CalDAVError> {
|
pub async fn discover_calendars(&self) -> Result<Vec<String>, CalDAVError> {
|
||||||
// First, try to discover user calendars if we have a calendar path in config
|
// First, try to discover user calendars if we have a calendar path in config
|
||||||
|
|||||||
61
backend/src/debug_caldav.rs
Normal file
61
backend/src/debug_caldav.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use crate::calendar::CalDAVClient;
|
||||||
|
use crate::config::CalDAVConfig;
|
||||||
|
|
||||||
|
pub async fn debug_caldav_fetch() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let config = CalDAVConfig::from_env()?;
|
||||||
|
let client = CalDAVClient::new(config);
|
||||||
|
|
||||||
|
println!("=== DEBUG: CalDAV Fetch ===");
|
||||||
|
|
||||||
|
// Discover calendars
|
||||||
|
let calendars = client.discover_calendars().await?;
|
||||||
|
println!("Found {} calendars: {:?}", calendars.len(), calendars);
|
||||||
|
|
||||||
|
if let Some(calendar_path) = calendars.first() {
|
||||||
|
println!("Fetching events from: {}", calendar_path);
|
||||||
|
|
||||||
|
// Make the raw REPORT request
|
||||||
|
let report_body = r#"<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||||
|
<d:prop>
|
||||||
|
<d:getetag/>
|
||||||
|
<c:calendar-data/>
|
||||||
|
</d:prop>
|
||||||
|
<c:filter>
|
||||||
|
<c:comp-filter name="VCALENDAR">
|
||||||
|
<c:comp-filter name="VEVENT"/>
|
||||||
|
</c:comp-filter>
|
||||||
|
</c:filter>
|
||||||
|
</c:calendar-query>"#;
|
||||||
|
|
||||||
|
let url = format!("{}{}", client.config.server_url.trim_end_matches('/'), calendar_path);
|
||||||
|
println!("Request URL: {}", url);
|
||||||
|
|
||||||
|
let response = client.http_client
|
||||||
|
.request(reqwest::Method::from_bytes(b"REPORT").unwrap(), &url)
|
||||||
|
.header("Authorization", format!("Basic {}", client.config.get_basic_auth()))
|
||||||
|
.header("Content-Type", "application/xml")
|
||||||
|
.header("Depth", "1")
|
||||||
|
.header("User-Agent", "calendar-app/0.1.0")
|
||||||
|
.body(report_body)
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("Response status: {}", response.status());
|
||||||
|
let body = response.text().await?;
|
||||||
|
println!("Response body length: {}", body.len());
|
||||||
|
println!("First 500 chars of response: {}", &body[..std::cmp::min(500, body.len())]);
|
||||||
|
|
||||||
|
// Try to parse it
|
||||||
|
let events = client.parse_calendar_response(&body)?;
|
||||||
|
println!("Parsed {} events", events.len());
|
||||||
|
|
||||||
|
for (i, event) in events.iter().enumerate() {
|
||||||
|
println!("Event {}: {}", i+1, event.summary.as_deref().unwrap_or("No title"));
|
||||||
|
println!(" Start: {}", event.start);
|
||||||
|
println!(" UID: {}", event.uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use crate::services::CalendarEvent;
|
use crate::services::{CalendarEvent, EventReminder, ReminderAction};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct EventModalProps {
|
pub struct EventModalProps {
|
||||||
@@ -39,6 +39,7 @@ pub fn EventModal(props: &EventModalProps) -> Html {
|
|||||||
<strong>{"Title:"}</strong>
|
<strong>{"Title:"}</strong>
|
||||||
<span>{event.get_title()}</span>
|
<span>{event.get_title()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
if let Some(ref description) = event.description {
|
if let Some(ref description) = event.description {
|
||||||
html! {
|
html! {
|
||||||
@@ -51,22 +52,12 @@ pub fn EventModal(props: &EventModalProps) -> Html {
|
|||||||
html! {}
|
html! {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
|
||||||
if let Some(ref location) = event.location {
|
|
||||||
html! {
|
|
||||||
<div class="event-detail">
|
|
||||||
<strong>{"Location:"}</strong>
|
|
||||||
<span>{location}</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
html! {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
<div class="event-detail">
|
<div class="event-detail">
|
||||||
<strong>{"Start:"}</strong>
|
<strong>{"Start:"}</strong>
|
||||||
<span>{format_datetime(&event.start, event.all_day)}</span>
|
<span>{format_datetime(&event.start, event.all_day)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
if let Some(ref end) = event.end {
|
if let Some(ref end) = event.end {
|
||||||
html! {
|
html! {
|
||||||
@@ -79,10 +70,140 @@ pub fn EventModal(props: &EventModalProps) -> Html {
|
|||||||
html! {}
|
html! {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"All Day:"}</strong>
|
||||||
|
<span>{if event.all_day { "Yes" } else { "No" }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
if let Some(ref location) = event.location {
|
||||||
|
html! {
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Location:"}</strong>
|
||||||
|
<span>{location}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<div class="event-detail">
|
<div class="event-detail">
|
||||||
<strong>{"Status:"}</strong>
|
<strong>{"Status:"}</strong>
|
||||||
<span>{&event.status}</span>
|
<span>{event.get_status_display()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Privacy:"}</strong>
|
||||||
|
<span>{event.get_class_display()}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Priority:"}</strong>
|
||||||
|
<span>{event.get_priority_display()}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
if let Some(ref organizer) = event.organizer {
|
||||||
|
html! {
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Organizer:"}</strong>
|
||||||
|
<span>{organizer}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
if !event.attendees.is_empty() {
|
||||||
|
html! {
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Attendees:"}</strong>
|
||||||
|
<span>{event.attendees.join(", ")}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
if !event.categories.is_empty() {
|
||||||
|
html! {
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Categories:"}</strong>
|
||||||
|
<span>{event.categories.join(", ")}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
if let Some(ref recurrence) = event.recurrence_rule {
|
||||||
|
html! {
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Repeats:"}</strong>
|
||||||
|
<span>{format_recurrence_rule(recurrence)}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Repeats:"}</strong>
|
||||||
|
<span>{"No"}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
if !event.reminders.is_empty() {
|
||||||
|
html! {
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Reminders:"}</strong>
|
||||||
|
<span>{format_reminders(&event.reminders)}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Reminders:"}</strong>
|
||||||
|
<span>{"None"}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
if let Some(ref created) = event.created {
|
||||||
|
html! {
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Created:"}</strong>
|
||||||
|
<span>{format_datetime(created, false)}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
if let Some(ref modified) = event.last_modified {
|
||||||
|
html! {
|
||||||
|
<div class="event-detail">
|
||||||
|
<strong>{"Last Modified:"}</strong>
|
||||||
|
<span>{format_datetime(modified, false)}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -99,3 +220,70 @@ fn format_datetime(dt: &DateTime<Utc>, all_day: bool) -> String {
|
|||||||
dt.format("%B %d, %Y at %I:%M %p").to_string()
|
dt.format("%B %d, %Y at %I:%M %p").to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn format_recurrence_rule(rrule: &str) -> String {
|
||||||
|
// Basic parsing of RRULE to display user-friendly text
|
||||||
|
if rrule.contains("FREQ=DAILY") {
|
||||||
|
"Daily".to_string()
|
||||||
|
} else if rrule.contains("FREQ=WEEKLY") {
|
||||||
|
"Weekly".to_string()
|
||||||
|
} else if rrule.contains("FREQ=MONTHLY") {
|
||||||
|
"Monthly".to_string()
|
||||||
|
} else if rrule.contains("FREQ=YEARLY") {
|
||||||
|
"Yearly".to_string()
|
||||||
|
} else {
|
||||||
|
// Show the raw rule if we can't parse it
|
||||||
|
format!("Custom ({})", rrule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_reminders(reminders: &[EventReminder]) -> String {
|
||||||
|
if reminders.is_empty() {
|
||||||
|
return "None".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted_reminders: Vec<String> = reminders
|
||||||
|
.iter()
|
||||||
|
.map(|reminder| {
|
||||||
|
let time_text = if reminder.minutes_before == 0 {
|
||||||
|
"At event time".to_string()
|
||||||
|
} else if reminder.minutes_before < 60 {
|
||||||
|
format!("{} minutes before", reminder.minutes_before)
|
||||||
|
} else if reminder.minutes_before == 60 {
|
||||||
|
"1 hour before".to_string()
|
||||||
|
} else if reminder.minutes_before % 60 == 0 {
|
||||||
|
format!("{} hours before", reminder.minutes_before / 60)
|
||||||
|
} else if reminder.minutes_before < 1440 {
|
||||||
|
let hours = reminder.minutes_before / 60;
|
||||||
|
let minutes = reminder.minutes_before % 60;
|
||||||
|
format!("{}h {}m before", hours, minutes)
|
||||||
|
} else if reminder.minutes_before == 1440 {
|
||||||
|
"1 day before".to_string()
|
||||||
|
} else if reminder.minutes_before % 1440 == 0 {
|
||||||
|
format!("{} days before", reminder.minutes_before / 1440)
|
||||||
|
} else {
|
||||||
|
let days = reminder.minutes_before / 1440;
|
||||||
|
let remaining_minutes = reminder.minutes_before % 1440;
|
||||||
|
let hours = remaining_minutes / 60;
|
||||||
|
let minutes = remaining_minutes % 60;
|
||||||
|
if hours > 0 {
|
||||||
|
format!("{}d {}h before", days, hours)
|
||||||
|
} else if minutes > 0 {
|
||||||
|
format!("{}d {}m before", days, minutes)
|
||||||
|
} else {
|
||||||
|
format!("{} days before", days)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let action_text = match reminder.action {
|
||||||
|
ReminderAction::Display => "notification",
|
||||||
|
ReminderAction::Email => "email",
|
||||||
|
ReminderAction::Audio => "sound",
|
||||||
|
};
|
||||||
|
|
||||||
|
format!("{} ({})", time_text, action_text)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
formatted_reminders.join(", ")
|
||||||
|
}
|
||||||
@@ -5,6 +5,26 @@ use wasm_bindgen_futures::JsFuture;
|
|||||||
use web_sys::{Request, RequestInit, RequestMode, Response};
|
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct EventReminder {
|
||||||
|
pub minutes_before: i32,
|
||||||
|
pub action: ReminderAction,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum ReminderAction {
|
||||||
|
Display,
|
||||||
|
Email,
|
||||||
|
Audio,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ReminderAction {
|
||||||
|
fn default() -> Self {
|
||||||
|
ReminderAction::Display
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub struct CalendarEvent {
|
pub struct CalendarEvent {
|
||||||
pub uid: String,
|
pub uid: String,
|
||||||
@@ -13,8 +33,45 @@ pub struct CalendarEvent {
|
|||||||
pub start: DateTime<Utc>,
|
pub start: DateTime<Utc>,
|
||||||
pub end: Option<DateTime<Utc>>,
|
pub end: Option<DateTime<Utc>>,
|
||||||
pub location: Option<String>,
|
pub location: Option<String>,
|
||||||
pub status: String,
|
pub status: EventStatus,
|
||||||
|
pub class: EventClass,
|
||||||
|
pub priority: Option<u8>,
|
||||||
|
pub organizer: Option<String>,
|
||||||
|
pub attendees: Vec<String>,
|
||||||
|
pub categories: Vec<String>,
|
||||||
|
pub created: Option<DateTime<Utc>>,
|
||||||
|
pub last_modified: Option<DateTime<Utc>>,
|
||||||
|
pub recurrence_rule: Option<String>,
|
||||||
pub all_day: bool,
|
pub all_day: bool,
|
||||||
|
pub reminders: Vec<EventReminder>,
|
||||||
|
pub etag: Option<String>,
|
||||||
|
pub href: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum EventStatus {
|
||||||
|
Tentative,
|
||||||
|
Confirmed,
|
||||||
|
Cancelled,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EventStatus {
|
||||||
|
fn default() -> Self {
|
||||||
|
EventStatus::Confirmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub enum EventClass {
|
||||||
|
Public,
|
||||||
|
Private,
|
||||||
|
Confidential,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EventClass {
|
||||||
|
fn default() -> Self {
|
||||||
|
EventClass::Public
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CalendarEvent {
|
impl CalendarEvent {
|
||||||
@@ -31,6 +88,38 @@ impl CalendarEvent {
|
|||||||
pub fn get_title(&self) -> String {
|
pub fn get_title(&self) -> String {
|
||||||
self.summary.clone().unwrap_or_else(|| "Untitled Event".to_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 {
|
||||||
|
EventStatus::Tentative => "Tentative",
|
||||||
|
EventStatus::Confirmed => "Confirmed",
|
||||||
|
EventStatus::Cancelled => "Cancelled",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get display string for class
|
||||||
|
pub fn get_class_display(&self) -> &'static str {
|
||||||
|
match self.class {
|
||||||
|
EventClass::Public => "Public",
|
||||||
|
EventClass::Private => "Private",
|
||||||
|
EventClass::Confidential => "Confidential",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CalendarService {
|
pub struct CalendarService {
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
pub mod calendar_service;
|
pub mod calendar_service;
|
||||||
|
|
||||||
pub use calendar_service::{CalendarService, CalendarEvent};
|
pub use calendar_service::{CalendarService, CalendarEvent, EventStatus, EventClass, EventReminder, ReminderAction};
|
||||||
1
test_backend_url.js
Normal file
1
test_backend_url.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
console.log("Backend URL test");
|
||||||
Reference in New Issue
Block a user