From 2f9622ab77e9baa82d65ae92525ae842de7ba757 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Mon, 15 Dec 2025 23:16:08 -0300 Subject: [PATCH] Update calendar and email modules --- src/calendar/mod.rs | 96 ++++++++++++++++--- src/email/mod.rs | 220 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 298 insertions(+), 18 deletions(-) diff --git a/src/calendar/mod.rs b/src/calendar/mod.rs index 02e5d9a4..3a0d05c8 100644 --- a/src/calendar/mod.rs +++ b/src/calendar/mod.rs @@ -249,10 +249,8 @@ pub async fn list_events( Json(vec![]) } -/// List calendars -pub async fn list_calendars( - State(_state): State>, -) -> Json { +/// List calendars - JSON API for services +pub async fn list_calendars_api(State(_state): State>) -> Json { Json(serde_json::json!({ "calendars": [ { @@ -265,16 +263,48 @@ pub async fn list_calendars( })) } -/// Get upcoming events -pub async fn upcoming_events( - State(_state): State>, -) -> Json { +/// List calendars - HTMX HTML response for UI +pub async fn list_calendars(State(_state): State>) -> axum::response::Html { + axum::response::Html(r#" +
+ + My Calendar +
+
+ + Work +
+
+ + Personal +
+ "#.to_string()) +} + +/// Get upcoming events - JSON API for services +pub async fn upcoming_events_api(State(_state): State>) -> Json { Json(serde_json::json!({ "events": [], "message": "No upcoming events" })) } +/// Get upcoming events - HTMX HTML response for UI +pub async fn upcoming_events(State(_state): State>) -> axum::response::Html { + axum::response::Html( + r#" +
+
+
+ No upcoming events + Create your first event +
+
+ "# + .to_string(), + ) +} + pub async fn get_event( State(_state): State>, Path(_id): Path, @@ -323,7 +353,43 @@ pub async fn import_ical( Ok(Json(serde_json::json!({ "imported": events.len() }))) } -pub fn router(state: Arc) -> Router { +/// New event form (HTMX HTML response) +pub async fn new_event_form(State(_state): State>) -> axum::response::Html { + axum::response::Html(r#" +
+

Create a new event using the form on the right panel.

+
+ "#.to_string()) +} + +/// New calendar form (HTMX HTML response) +pub async fn new_calendar_form(State(_state): State>) -> axum::response::Html { + axum::response::Html(r#" +
+
+ + +
+
+ +
+ + + + + +
+
+
+ + +
+
+ "#.to_string()) +} + +/// Configure calendar API routes +pub fn configure_calendar_routes() -> Router> { Router::new() .route( ApiUrls::CALENDAR_EVENTS, @@ -335,10 +401,14 @@ pub fn router(state: Arc) -> Router { ) .route("/api/calendar/export.ics", get(export_ical)) .route("/api/calendar/import", post(import_ical)) - // UI-compatible endpoints - .route("/api/calendar/list", get(list_calendars)) - .route("/api/calendar/upcoming", get(upcoming_events)) - .with_state(state) + // JSON API endpoints for services + .route("/api/calendar/calendars", get(list_calendars_api)) + .route("/api/calendar/events/upcoming", get(upcoming_events_api)) + // HTMX UI endpoints (return HTML fragments) + .route("/ui/calendar/list", get(list_calendars)) + .route("/ui/calendar/upcoming", get(upcoming_events)) + .route("/ui/calendar/event/new", get(new_event_form)) + .route("/ui/calendar/new", get(new_calendar_form)) } #[cfg(test)] diff --git a/src/email/mod.rs b/src/email/mod.rs index 12dfd29a..209e2ba7 100644 --- a/src/email/mod.rs +++ b/src/email/mod.rs @@ -36,6 +36,7 @@ async fn extract_user_from_session(state: &Arc) -> Result Router> { Router::new() + // JSON API endpoints for services .route(ApiUrls::EMAIL_ACCOUNTS, get(list_email_accounts)) .route( &format!("{}/add", ApiUrls::EMAIL_ACCOUNTS), @@ -45,11 +46,9 @@ pub fn configure() -> Router> { ApiUrls::EMAIL_ACCOUNT_BY_ID.replace(":id", "{account_id}"), axum::routing::delete(delete_email_account), ) - .route(ApiUrls::EMAIL_LIST, get(list_emails_htmx).post(list_emails)) + .route(ApiUrls::EMAIL_LIST, post(list_emails)) .route(ApiUrls::EMAIL_SEND, post(send_email)) .route(ApiUrls::EMAIL_DRAFT, post(save_draft)) - .route("/api/email/folders", get(list_folders_htmx)) - .route("/api/email/compose", get(compose_email_htmx)) .route( ApiUrls::EMAIL_FOLDERS.replace(":account_id", "{account_id}"), get(list_folders), @@ -65,13 +64,24 @@ pub fn configure() -> Router> { .replace(":email", "{email}"), post(track_click), ) - .route("/api/email/:id", get(get_email_content_htmx)) - .route("/api/email/:id", delete(delete_email_htmx)) // Email read tracking endpoints .route("/api/email/tracking/pixel/{tracking_id}", get(serve_tracking_pixel)) .route("/api/email/tracking/status/{tracking_id}", get(get_tracking_status)) .route("/api/email/tracking/list", get(list_sent_emails_tracking)) .route("/api/email/tracking/stats", get(get_tracking_stats)) + // UI HTMX endpoints (return HTML fragments) + .route("/ui/email/accounts", get(list_email_accounts_htmx)) + .route("/ui/email/list", get(list_emails_htmx)) + .route("/ui/email/folders", get(list_folders_htmx)) + .route("/ui/email/compose", get(compose_email_htmx)) + .route("/ui/email/:id", get(get_email_content_htmx)) + .route("/ui/email/:id/delete", delete(delete_email_htmx)) + .route("/ui/email/labels", get(list_labels_htmx)) + .route("/ui/email/templates", get(list_templates_htmx)) + .route("/ui/email/signatures", get(list_signatures_htmx)) + .route("/ui/email/rules", get(list_rules_htmx)) + .route("/ui/email/search", get(search_emails_htmx)) + .route("/ui/email/auto-responder", post(save_auto_responder)) } // Export SaveDraftRequest for other modules @@ -383,6 +393,63 @@ pub async fn add_email_account( })) } +/// List email accounts - HTMX HTML response for UI +pub async fn list_email_accounts_htmx( + State(state): State>, +) -> impl IntoResponse { + // Get user_id from session + let user_id = match extract_user_from_session(&state).await { + Ok(id) => id, + Err(_) => { + return axum::response::Html(r#" + + "#.to_string()); + } + }; + + let conn = state.conn.clone(); + let accounts = tokio::task::spawn_blocking(move || { + let mut db_conn = conn.get().map_err(|e| format!("DB connection error: {}", e))?; + + diesel::sql_query( + "SELECT id, email, display_name, is_primary FROM user_email_accounts WHERE user_id = $1 AND is_active = true ORDER BY is_primary DESC" + ) + .bind::(user_id) + .load::<(Uuid, String, Option, bool)>(&mut db_conn) + .map_err(|e| format!("Query failed: {}", e)) + }) + .await + .ok() + .and_then(|r| r.ok()) + .unwrap_or_default(); + + if accounts.is_empty() { + return axum::response::Html(r#" + + "#.to_string()); + } + + let mut html = String::new(); + for (id, email, display_name, is_primary) in accounts { + let name = display_name.unwrap_or_else(|| email.clone()); + let primary_badge = if is_primary { r#"Primary"# } else { "" }; + html.push_str(&format!( + r#""#, + id, name, primary_badge + )); + } + + axum::response::Html(html) +} + +/// List email accounts - JSON API for services pub async fn list_email_accounts( State(state): State>, ) -> Result>>, EmailError> { @@ -2066,3 +2133,146 @@ struct EmailAccountRow { #[diesel(sql_type = diesel::sql_types::Integer)] pub smtp_port: i32, } + +// ===== HTMX UI Endpoint Handlers ===== + +/// List email labels (HTMX HTML response) +pub async fn list_labels_htmx( + State(_state): State>, +) -> impl IntoResponse { + // Return default labels as HTML for HTMX + axum::response::Html(r#" +
+ + Important +
+
+ + Work +
+
+ + Personal +
+
+ + Finance +
+ "#.to_string()) +} + +/// List email templates (HTMX HTML response) +pub async fn list_templates_htmx( + State(_state): State>, +) -> impl IntoResponse { + axum::response::Html(r#" +
+

Welcome Email

+

Standard welcome message for new contacts

+
+
+

Follow Up

+

General follow-up template

+
+
+

Meeting Request

+

Request a meeting with scheduling options

+
+

+ Click a template to use it +

+ "#.to_string()) +} + +/// List email signatures (HTMX HTML response) +pub async fn list_signatures_htmx( + State(_state): State>, +) -> impl IntoResponse { + axum::response::Html(r#" +
+

Default Signature

+

Best regards,
Your Name

+
+
+

Formal Signature

+

Sincerely,
Your Name
Title | Company

+
+

+ Click a signature to insert it +

+ "#.to_string()) +} + +/// List email rules (HTMX HTML response) +pub async fn list_rules_htmx( + State(_state): State>, +) -> impl IntoResponse { + axum::response::Html(r#" +
+
+ Auto-archive newsletters + +
+

From: *@newsletter.* → Archive

+
+
+
+ Label work emails + +
+

From: *@company.com → Label: Work

+
+ + "#.to_string()) +} + +/// Search emails (HTMX HTML response) +pub async fn search_emails_htmx( + State(state): State>, + Query(params): Query>, +) -> impl IntoResponse { + let query = params.get("q").map(|s| s.as_str()).unwrap_or(""); + + if query.is_empty() { + return axum::response::Html(r#" +
+

Enter a search term to find emails

+
+ "#.to_string()); + } + + // For now, return a placeholder - in production this would search the database + axum::response::Html(format!(r#" +
+ + + + +

Searching for "{}"

+

No results found. Try different keywords.

+
+ "#, query)) +} + +/// Save auto-responder settings +pub async fn save_auto_responder( + State(_state): State>, + axum::Form(form): axum::Form>, +) -> impl IntoResponse { + info!("Saving auto-responder settings: {:?}", form); + + // In production, save to database + axum::response::Html(r#" +
+ Auto-responder settings saved successfully! +
+ "#.to_string()) +}